openapi: "3.1.0"
info:
  title: "WhereNext Relocation Intelligence API"
  description: |
    Free, open data APIs for international relocation decisions.
    95 countries, 380 cities, 4,149 schools, 27 institutional data sources.
    All data CC BY 4.0 unless noted otherwise.

    **Composite indexes:** Global Relocation Index (7 dimensions) and Cost of Living Index (5 sub-indexes).

    **Data sources:** World Bank, OECD, WHO, UNDP, Global Peace Index, Transparency International, ILO, Eurostat, PISA, EF EPI, Open-Meteo, and WhereNext curated research.

    **Citation format:** "WhereNext. [Dataset Name] 2026. getwherenext.com/data/[dataset]. CC BY 4.0."

    Documentation: https://getwherenext.com/data
    Methodology: https://getwherenext.com/methodology
    LLM context file: https://getwherenext.com/llms.txt
  version: "2026.05"
  contact:
    email: "hello@getwherenext.com"
    url: "https://getwherenext.com/data"
  license:
    name: "CC BY 4.0"
    url: "https://creativecommons.org/licenses/by/4.0/"

servers:
  - url: "https://getwherenext.com"
    description: "Production"

tags:
  - name: "Relocation Index"
    description: "Global Relocation Index — 95 countries ranked across 7 dimensions"
  - name: "Cost of Living"
    description: "Cost of living data at country and city level"
  - name: "Tax"
    description: "Expat tax rates and tax optimization data"
  - name: "Visa"
    description: "Visa requirements, passport power, and digital nomad visas"
  - name: "Schools"
    description: "International school costs, class sizes, diversity, and affordability"
  - name: "Property"
    description: "Property buying costs, yields, and mortgage access for foreign buyers"
  - name: "AI-Optimized"
    description: "Structured endpoints designed for AI/LLM consumption with JSON-LD and natural language summaries"
  - name: "Product Feed"
    description: "Google Shopping product feed for premium reports"

paths:
  /api/data/destination-summaries:
    get:
      operationId: getDestinationSummaries
      summary: "AI-Readable Destination Summary Dataset v1"
      description: |
        Compact JSON summary of priority countries + cities for AI grounding.
        Each entity includes: viability summary (≤320 chars), top 3 strengths,
        top 2 blockers (with free WhereNext tool routes), key dimension metrics,
        source labels, last_verified date, confidence band, and recommended
        next actions (start_case / tool / compare). Schema v1.0. CC BY 4.0.
        5 countries + 5 cities at launch. Expansion gated on consumer signal.
      tags: ["Destination Summaries"]
      responses:
        "200":
          description: "Destination summaries payload"
          headers:
            Cache-Control:
              schema:
                type: string
                example: "public, max-age=86400"
            Access-Control-Allow-Origin:
              schema:
                type: string
                example: "*"
            X-Schema-Version:
              schema:
                type: string
                example: "1.0"
          content:
            application/json:
              schema:
                type: object
                properties:
                  schema_version:
                    type: string
                    example: "1.0"
                  generated_at:
                    type: string
                    format: date-time
                  data_last_updated:
                    type: string
                    format: date
                  license:
                    type: string
                    example: "CC BY 4.0"
                  citation:
                    type: string
                  count:
                    type: object
                    properties:
                      countries:
                        type: integer
                      cities:
                        type: integer
                  entities:
                    type: array
                    items:
                      type: object
                      properties:
                        schema_version:
                          type: string
                        entity_type:
                          type: string
                          enum: [country, city]
                        slug:
                          type: string
                        country_code:
                          type: string
                          pattern: "^[A-Z]{2}$"
                        display_name:
                          type: string
                        summary:
                          type: string
                          maxLength: 320
                        best_fit_personas:
                          type: array
                          items: { type: string }
                        strengths:
                          type: array
                          maxItems: 3
                        blockers:
                          type: array
                          maxItems: 2
                        key_metrics:
                          type: object
                        sources:
                          type: array
                          items: { type: string }
                        last_verified:
                          type: string
                        confidence:
                          type: string
                          enum: [high, medium, low]
                        recommended_next_actions:
                          type: array
                          minItems: 1
                          maxItems: 4
                        canonical_url:
                          type: string
                          format: uri

  /api/data/relocation-index:
    get:
      operationId: getRelocationIndex
      summary: "Global Relocation Index 2026"
      description: |
        Returns the full Global Relocation Index ranking 95 countries across 7 dimensions:
        cost, safety, healthcare, education, career, lifestyle, and infrastructure.
        Scores are 0-100 normalized within each dimension. Composite score is confidence-weighted.
      tags: ["Relocation Index"]
      responses:
        "200":
          description: "Relocation index with country rankings"
          headers:
            Cache-Control:
              schema:
                type: string
                example: "public, max-age=86400"
            Access-Control-Allow-Origin:
              schema:
                type: string
                example: "*"
          content:
            application/json:
              schema:
                type: object
                properties:
                  meta:
                    $ref: "#/components/schemas/RelocationIndexMeta"
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/RelocationIndexRow"
              example:
                meta:
                  title: "2026 Global Relocation Index"
                  version: "2026.1"
                  source: "WhereNext (getwherenext.com)"
                  license: "CC BY 4.0"
                  updated: "2026-03-07"
                  countries: 95
                  dimensions: ["cost", "safety", "healthcare", "education", "career", "lifestyle", "infrastructure"]
                  methodology: "https://getwherenext.com/methodology"
                  csv: "https://getwherenext.com/api/data/relocation-index/csv"
                data:
                  - rank: 1
                    country_code: "ch"
                    country: "Switzerland"
                    region: "Europe"
                    composite_score: 82
                    cost: 18
                    safety: 92
                    healthcare: 95
                    education: 88
                    career: 85
                    lifestyle: 80
                    infrastructure: 90
                    confidence: 0.95

  /api/data/relocation-index/csv:
    get:
      operationId: getRelocationIndexCsv
      summary: "Global Relocation Index 2026 (CSV)"
      description: |
        CSV download of the Global Relocation Index.
        Columns: rank, country_code, country, region, composite_score, cost, safety, healthcare, education, career, lifestyle, infrastructure, confidence.
      tags: ["Relocation Index"]
      responses:
        "200":
          description: "CSV file download"
          headers:
            Content-Disposition:
              schema:
                type: string
                example: 'attachment; filename="wherenext-global-relocation-index-2026.csv"'
          content:
            text/csv:
              schema:
                type: string

  /api/data/cost-of-living:
    get:
      operationId: getCostOfLiving
      summary: "Cost of Living Index 2026"
      description: |
        Returns cost of living data for 95 countries, ranked cheapest to most expensive.
        Includes overall cost index, monthly USD estimate, and sub-indexes for groceries, rent, utilities, and transport.
      tags: ["Cost of Living"]
      responses:
        "200":
          description: "Cost of living rankings"
          content:
            application/json:
              schema:
                type: object
                properties:
                  meta:
                    $ref: "#/components/schemas/CostOfLivingMeta"
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/CostOfLivingRow"
              example:
                meta:
                  title: "2026 Cost of Living Index"
                  source: "WhereNext (getwherenext.com)"
                  license: "CC BY 4.0"
                  updated: "2026-03-03"
                  countries: 95
                  csv: "https://getwherenext.com/api/data/cost-of-living/csv"
                data:
                  - rank: 1
                    country_code: "VN"
                    country: "Vietnam"
                    region: "Asia"
                    cost_index: 28
                    monthly_estimate_usd: 1024
                    grocery_index: 25
                    rent_index: 15
                    utilities_index: 30
                    transport_index: 20

  /api/data/cost-of-living/csv:
    get:
      operationId: getCostOfLivingCsv
      summary: "Cost of Living Index 2026 (CSV)"
      description: |
        CSV download of the Cost of Living Index.
        Columns: rank, country_code, country, region, cost_index, monthly_estimate_usd, grocery_index, rent_index, utilities_index, transport_index.
      tags: ["Cost of Living"]
      responses:
        "200":
          description: "CSV file download"
          headers:
            Content-Disposition:
              schema:
                type: string
                example: 'attachment; filename="wherenext-cost-of-living-2026.csv"'
          content:
            text/csv:
              schema:
                type: string

  /api/data/expat-tax-rates:
    get:
      operationId: getExpatTaxRates
      summary: "Expat Tax Rates by Country 2026"
      description: |
        Effective income tax + social contribution rates for expats across 20 popular relocation destinations.
        Rates calculated at three income tiers: $50,000, $100,000, and $200,000.
        Single filer, employment income, tax resident. Countries sorted by effective rate at $100K.
      tags: ["Tax"]
      responses:
        "200":
          description: "Tax rate data for 20 countries"
          content:
            application/json:
              schema:
                type: object
                properties:
                  dataset:
                    type: string
                    example: "expat-tax-rates-2026"
                  description:
                    type: string
                  methodology:
                    type: string
                  license:
                    type: string
                    example: "CC BY 4.0"
                  source:
                    type: string
                    format: uri
                  updated:
                    type: string
                    format: date
                  countries:
                    type: array
                    items:
                      $ref: "#/components/schemas/ExpatTaxCountry"
              example:
                dataset: "expat-tax-rates-2026"
                license: "CC BY 4.0"
                updated: "2026-03-31"
                countries:
                  - countryCode: "AE"
                    countryName: "United Arab Emirates"
                    rates:
                      - grossIncome: 50000
                        totalTax: 0
                        effectiveRate: 0
                        incomeTax: 0
                        socialContributions: 0

  /api/data/inheritance-tax:
    get:
      operationId: getInheritanceTax
      summary: "Inheritance & Estate Tax by Country 2026"
      description: |
        Comparative inheritance & estate tax across 24 relocation destinations: estate vs
        beneficiary model, spouse/child treatment, top distant-heir rates, and non-resident
        (worldwide vs situs) basis. Append ?format=intelligence for the canonical intelligence contract.
      tags: ["Tax"]
      responses:
        "200":
          description: "Inheritance/estate tax data for 24 countries"
          content:
            application/json:
              schema:
                type: object
  /api/data/inheritance-tax/csv:
    get:
      operationId: getInheritanceTaxCsv
      summary: "Inheritance & Estate Tax by Country 2026 (CSV)"
      description: |
        CSV export of comparative inheritance & estate tax across 24 relocation destinations
        (one row per country: model, spouse/child treatment, top rates, non-resident basis).
      tags: ["Tax"]
      responses:
        "200":
          description: "CSV file download"
          headers:
            Content-Disposition:
              schema:
                type: string
                example: 'attachment; filename="wherenext-inheritance-tax-2026.csv"'
          content:
            text/csv:
              schema:
                type: string

  /api/data/digital-nomad-visas:
    get:
      operationId: getDigitalNomadVisas
      summary: "Digital Nomad Visa Index 2026"
      description: |
        24 countries with active digital nomad visa programs.
        Includes cost, income requirements, duration, tax treatment, processing time, and key requirements.
      tags: ["Visa"]
      responses:
        "200":
          description: "Digital nomad visa programs"
          content:
            application/json:
              schema:
                type: object
                properties:
                  dataset:
                    type: string
                    example: "digital-nomad-visas-2026"
                  description:
                    type: string
                  license:
                    type: string
                    example: "CC BY 4.0"
                  updated:
                    type: string
                    format: date
                  count:
                    type: integer
                  visas:
                    type: array
                    items:
                      $ref: "#/components/schemas/DigitalNomadVisa"
              example:
                dataset: "digital-nomad-visas-2026"
                license: "CC BY 4.0"
                count: 24
                visas:
                  - countryCode: "PT"
                    countryName: "Portugal"
                    visaName: "Digital Nomad Visa (D8)"
                    region: "Europe"
                    durationMonths: 12
                    renewable: true
                    maxStayMonths: null
                    costUsd: 83
                    minIncomeUsdYear: 42120
                    processingDays: 60
                    taxTreatment: "taxed-if-resident"
                    remoteWorkAllowed: true

  /api/data/property-costs:
    get:
      operationId: getPropertyCosts
      summary: "Property Buying Costs by Country 2026"
      description: |
        Property buying costs, taxes, mortgage access, rental yields, and capital gains rates
        for foreign buyers across 19 popular relocation destinations.
        Supports optional `?format=csv` query parameter for CSV download.
      tags: ["Property"]
      parameters:
        - name: format
          in: query
          required: false
          description: "Set to 'csv' for CSV download"
          schema:
            type: string
            enum: ["csv"]
      responses:
        "200":
          description: "Property cost data (JSON or CSV depending on format param)"
          content:
            application/json:
              schema:
                type: object
                properties:
                  dataset:
                    type: string
                    example: "property-costs-2026"
                  description:
                    type: string
                  license:
                    type: string
                    example: "CC BY 4.0"
                  updated:
                    type: string
                    format: date
                  countryCount:
                    type: integer
                  countries:
                    type: array
                    items:
                      $ref: "#/components/schemas/PropertyCostCountry"
              example:
                dataset: "property-costs-2026"
                license: "CC BY 4.0"
                updated: "2026-04-03"
                countryCount: 19
                countries:
                  - countryCode: "PT"
                    countryName: "Portugal"
                    foreignOwnership: "unrestricted"
                    closingCostMinPct: 7
                    closingCostMaxPct: 12
                    mortgageAvailable: true
                    avgGrossYieldPct: 4.5
                    avgPricePerSqmUsd: 3200
                    residencyViaProperty: true
            text/csv:
              schema:
                type: string

  /api/data/median-salaries:
    get:
      operationId: getMedianSalaries
      summary: "Median Salaries by Country 2026"
      description: |
        National median annual salary in USD for 43 countries — gross, after-tax net, and PPP-adjusted
        purchasing power. Includes data year per country, months of local cost of living covered, and
        optional source notes. Data vintage 2022–2024 from OECD, Eurostat, ILO, and national
        statistical offices. Supports ?format=csv for CSV download.
      tags:
        - salary
        - country
      parameters:
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum: [json, csv]
          description: "Response format. Default: json. Use csv for CSV download."
      responses:
        "200":
          description: "Median salary data for 43 countries."
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        code:
                          type: string
                          example: "PT"
                        name:
                          type: string
                          example: "Portugal"
                        grossUsd:
                          type: number
                          example: 24000
                        netUsd:
                          type: number
                          example: 18000
                        pppAdjustedUsd:
                          type: number
                          example: 20000
            text/csv:
              schema:
                type: string

  /api/data/visa-requirements:
    get:
      operationId: getVisaRequirements
      summary: "Visa Requirements & Passport Power Index 2026"
      description: |
        Without `passport` param: returns passport power summary for all 95 nationalities.
        With `?passport=XX`: returns visa requirements for all 95 destinations from that passport (ISO 3166-1 alpha-2).
        Access levels: freedom-of-movement, visa-free, visa-on-arrival, e-visa, visa-required.
      tags: ["Visa"]
      parameters:
        - name: passport
          in: query
          required: false
          description: "ISO 3166-1 alpha-2 country code (e.g., US, GB, DE). If omitted, returns passport power summary for all nationalities."
          schema:
            type: string
            pattern: "^[A-Za-z]{2,3}$"
            example: "US"
      responses:
        "200":
          description: "Visa requirements or passport power index"
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/VisaRequirementsPerPassport"
                  - $ref: "#/components/schemas/PassportPowerIndex"
              examples:
                perPassport:
                  summary: "Per-passport visa requirements (?passport=US)"
                  value:
                    metadata:
                      title: "Visa Requirements for US Passport Holders (2026)"
                      license: "CC BY 4.0"
                      passport_code: "us"
                      total_destinations: 95
                    summary:
                      visa_free: 62
                      visa_on_arrival: 12
                      e_visa: 8
                      visa_required: 13
                      digital_nomad_visas: 18
                    data:
                      - destination_code: "pt"
                        destination: "Portugal"
                        access_level: "visa-free"
                        max_stay_days: 90
                        work_allowed: false
                        digital_nomad_visa: true
                        path_to_residency: true
                passportPower:
                  summary: "Passport power index (no param)"
                  value:
                    metadata:
                      title: "Passport Power Index 2026"
                      license: "CC BY 4.0"
                      total_passports: 95
                    data:
                      - passport_code: "de"
                        country: "Germany"
                        visa_free_destinations: 72
                        total_destinations: 95
                        passport_power_rank: 1

  /api/data/visa-requirements/csv:
    get:
      operationId: getVisaRequirementsCsv
      summary: "Passport Power Index 2026 (CSV)"
      description: |
        CSV download of passport power summary for all 95 nationalities.
        Columns: rank, passport_code, country, visa_free, visa_on_arrival, e_visa, visa_required, digital_nomad_visas, total_destinations.
      tags: ["Visa"]
      responses:
        "200":
          description: "CSV file download"
          headers:
            Content-Disposition:
              schema:
                type: string
                example: 'attachment; filename="wherenext-passport-power-2026.csv"'
          content:
            text/csv:
              schema:
                type: string

  /api/data/expat-tax-rates/csv:
    get:
      operationId: getExpatTaxRatesCsv
      summary: "Expat Tax Rates by Country 2026 (CSV)"
      description: |
        CSV download of effective expat tax rates at $50K/$100K/$200K income across 20 popular destinations.
        20 countries × 3 income tiers = 60 rows. Includes effective rate, income tax, social contributions.
        Excludes special expat regimes (Beckham, IFICI, NHR2, Lump-Sum, Impatriate) — those documented at /methodology#tax.
      tags: ["Tax"]
      responses:
        "200":
          description: "CSV file download"
          headers:
            Content-Disposition:
              schema:
                type: string
                example: 'attachment; filename="wherenext-expat-tax-rates-2026.csv"'
          content:
            text/csv:
              schema:
                type: string

  /api/data/digital-nomad-visas/csv:
    get:
      operationId: getDigitalNomadVisasCsv
      summary: "Digital Nomad Visa Index 2026 (CSV)"
      description: |
        CSV download of active DN visa programs (sorted by cost ascending).
        Columns: country_code, country, visa_name, duration_months, renewable, cost_usd, min_income_usd_year, processing_days, tax_treatment, tax_note.
      tags: ["Visa"]
      responses:
        "200":
          description: "CSV file download"
          headers:
            Content-Disposition:
              schema:
                type: string
                example: 'attachment; filename="wherenext-digital-nomad-visas-2026.csv"'
          content:
            text/csv:
              schema:
                type: string

  /api/data/school-costs:
    get:
      operationId: getSchoolCosts
      summary: "International School Costs by City"
      description: |
        Without `city` param: returns per-city school cost summaries for all 342 cities with schools.
        With `?city=dubai`: returns aggregated cost summary for a specific city (no per-school data exposed).
        Queries the school database directly (4,149 schools across 78 countries).
      tags: ["Schools"]
      parameters:
        - name: city
          in: query
          required: false
          description: "City slug (e.g., dubai, lisbon, bangkok, ho-chi-minh). If omitted, returns all cities."
          schema:
            type: string
            example: "dubai"
      responses:
        "200":
          description: "School cost summaries"
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/SchoolCostsAllCities"
                  - $ref: "#/components/schemas/SchoolCostsSingleCity"
              examples:
                allCities:
                  summary: "All cities (no param)"
                  value:
                    metadata:
                      title: "International School Costs by City (2026)"
                      license: "CC BY 4.0"
                      total_cities: 333
                      total_schools: 4149
                    data:
                      - city: "dubai"
                        city_name: "Dubai"
                        country: "United Arab Emirates"
                        country_code: "ae"
                        total_schools: 210
                        tuition_min_usd: 2500
                        tuition_max_usd: 45000
                        tuition_median_usd: 12000
                        curricula: ["American", "British", "IB"]
                singleCity:
                  summary: "Single city (?city=dubai)"
                  value:
                    metadata:
                      title: "International School Costs in Dubai, United Arab Emirates (2026)"
                      license: "CC BY 4.0"
                      city: "dubai"
                      country_code: "ae"
                    summary:
                      total_schools: 210
                      curricula: ["American", "British", "IB", "French"]
                      tuition_range_usd:
                        min: 2500
                        max: 45000
                        median: 12000
                      with_learning_support: 85
        "404":
          description: "City not found"
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string

  # Note: /api/data/school-costs/csv was removed (HTTP 410 Gone). It is
  # intentionally not documented here. Use /api/data/school-costs-index
  # with ?format=csv instead.

  /api/data/school-costs-index:
    get:
      operationId: getSchoolCostsIndex
      summary: "International School Cost Index 2026"
      description: |
        City-level school cost rankings with percentile distributions (P10, P25, median, P75, P90).
        Returns top 30 most expensive and top 30 most affordable cities. Full rankings on the data page.
        Supports `?format=csv` for CSV download.
        Minimum 3 schools per city to be included.
      tags: ["Schools"]
      parameters:
        - name: format
          in: query
          required: false
          description: "Set to 'csv' for CSV download"
          schema:
            type: string
            enum: ["csv"]
      responses:
        "200":
          description: "School cost index with city rankings"
          content:
            application/json:
              schema:
                type: object
                properties:
                  metadata:
                    type: object
                    properties:
                      title:
                        type: string
                      source:
                        type: string
                      license:
                        type: string
                      updated:
                        type: string
                        format: date
                      total_schools_with_fees:
                        type: integer
                      total_cities_ranked:
                        type: integer
                      total_countries:
                        type: integer
                      global_median_tuition_usd:
                        type: integer
                      global_p10_tuition_usd:
                        type: integer
                      global_p90_tuition_usd:
                        type: integer
                  top_expensive:
                    type: array
                    items:
                      $ref: "#/components/schemas/SchoolCostIndexCity"
                  top_affordable:
                    type: array
                    items:
                      $ref: "#/components/schemas/SchoolCostIndexCity"
              example:
                metadata:
                  title: "International School Cost Index 2026"
                  license: "CC BY 4.0"
                  total_schools_with_fees: 2900
                  total_cities_ranked: 149
                  global_median_tuition_usd: 12000
                top_expensive:
                  - rank: 1
                    city_slug: "zurich"
                    city_name: "Zurich"
                    country: "Switzerland"
                    country_code: "ch"
                    school_count: 15
                    median_tuition_usd: 32000
                    p25_tuition_usd: 25000
                    p75_tuition_usd: 40000
                    curricula: ["IB", "German", "British"]
            text/csv:
              schema:
                type: string

  /api/data/school-affordability:
    get:
      operationId: getSchoolAffordability
      summary: "International School Affordability Map 2026"
      description: |
        Schools-per-budget tiers across 200+ cities.
        Returns global bracket distribution (under $5K, $5-10K, $10-15K, $15-25K, $25-40K, over $40K)
        plus top 30 most affordable cities at $15K and $25K budget tiers.
        Uses WhereNext's true annual cost (including hidden fees) when available.
      tags: ["Schools"]
      responses:
        "200":
          description: "School affordability data"
          content:
            application/json:
              schema:
                type: object
                properties:
                  metadata:
                    type: object
                    properties:
                      title:
                        type: string
                      license:
                        type: string
                      total_schools_analyzed:
                        type: integer
                      total_cities:
                        type: integer
                      total_countries:
                        type: integer
                      schools_under_10k:
                        type: integer
                      schools_under_15k:
                        type: integer
                      schools_under_25k:
                        type: integer
                  bracket_distribution:
                    type: array
                    items:
                      $ref: "#/components/schemas/BudgetBracket"
                  top_cities_under_15k:
                    type: array
                    items:
                      $ref: "#/components/schemas/AffordableCity"
                  top_cities_under_25k:
                    type: array
                    items:
                      $ref: "#/components/schemas/AffordableCity"
              example:
                metadata:
                  title: "International School Affordability Map 2026"
                  license: "CC BY 4.0"
                  total_schools_analyzed: 2900
                  schools_under_10k: 850
                bracket_distribution:
                  - bracket: "Under $5,000/year"
                    bracket_key: "under_5k"
                    school_count: 320
                    city_count: 80
                    country_count: 30
                    pct_of_total: 11

  /api/data/school-class-sizes:
    get:
      operationId: getSchoolClassSizes
      summary: "International School Class Sizes 2026"
      description: |
        Average class sizes for international schools across 250+ cities.
        Returns top 30 cities with the smallest median class sizes.
        Minimum 3 schools per city with reported class sizes to be included.
      tags: ["Schools"]
      responses:
        "200":
          description: "Class size data by city"
          content:
            application/json:
              schema:
                type: object
                properties:
                  metadata:
                    type: object
                    properties:
                      title:
                        type: string
                      license:
                        type: string
                      total_schools_with_class_size:
                        type: integer
                      total_cities_ranked:
                        type: integer
                      total_countries:
                        type: integer
                      global_median_class_size:
                        type: integer
                      global_min_class_size:
                        type: integer
                      global_max_class_size:
                        type: integer
                  top_smallest_class_cities:
                    type: array
                    items:
                      $ref: "#/components/schemas/ClassSizeCity"
              example:
                metadata:
                  title: "International School Class Sizes 2026"
                  license: "CC BY 4.0"
                  global_median_class_size: 20
                top_smallest_class_cities:
                  - rank: 1
                    city_slug: "vienna"
                    city_name: "Vienna"
                    country: "Austria"
                    country_code: "at"
                    school_count: 12
                    median_class_size: 15
                    smallest_class_size: 8
                    largest_class_size: 22

  /api/data/school-diversity:
    get:
      operationId: getSchoolDiversity
      summary: "International School Diversity Index 2026"
      description: |
        Student nationality counts and diversity scores for international schools across 200+ cities.
        Returns top 30 most diverse cities by median number of nationalities per school.
        Minimum 3 schools per city with nationality data to be included.
      tags: ["Schools"]
      responses:
        "200":
          description: "Diversity data by city"
          content:
            application/json:
              schema:
                type: object
                properties:
                  metadata:
                    type: object
                    properties:
                      title:
                        type: string
                      license:
                        type: string
                      total_schools_with_nationality_data:
                        type: integer
                      total_cities_ranked:
                        type: integer
                      total_countries:
                        type: integer
                      global_median_nationalities:
                        type: integer
                      global_max_nationalities:
                        type: integer
                  top_diverse_cities:
                    type: array
                    items:
                      $ref: "#/components/schemas/DiverseCity"
              example:
                metadata:
                  title: "International School Diversity Index 2026"
                  license: "CC BY 4.0"
                  global_median_nationalities: 45
                top_diverse_cities:
                  - rank: 1
                    city_slug: "dubai"
                    city_name: "Dubai"
                    country: "United Arab Emirates"
                    country_code: "ae"
                    school_count: 180
                    median_nationalities: 72
                    max_nationalities: 105
                    median_diversity_score: 85

  /api/data/city-prices:
    get:
      operationId: getCityPrices
      summary: "City-Level Item Prices 2026"
      description: |
        Without `city` param: returns list of available cities with summary stats.
        With `?city=PT-Lisbon`: returns item-level prices (bread, milk, rent, transport, etc.) in USD and local currency.
        City keys use the format `{country_code}-{city_name}` (e.g., PT-Lisbon, TH-Bangkok).
        50+ cities with detailed price data.
      tags: ["Cost of Living"]
      parameters:
        - name: city
          in: query
          required: false
          description: "City key in format `{country_code}-{city_name}` (e.g., PT-Lisbon, TH-Bangkok). If omitted, returns available cities."
          schema:
            type: string
            example: "PT-Lisbon"
      responses:
        "200":
          description: "City price data"
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/CityPricesDetail"
                  - $ref: "#/components/schemas/CityPricesList"
              examples:
                cityDetail:
                  summary: "Item-level prices (?city=PT-Lisbon)"
                  value:
                    metadata:
                      title: "Item-Level Prices in Lisbon (2026)"
                      license: "CC BY 4.0"
                      city: "PT-Lisbon"
                      currency: "EUR"
                      exchange_rate: 0.92
                    data:
                      - category: "Groceries"
                        item: "Bread (500g loaf)"
                        price_usd: 1.52
                        price_local: 1.40
                      - category: "Transport"
                        item: "Monthly pass"
                        price_usd: 43.48
                        price_local: 40.00
                cityList:
                  summary: "Available cities (no param)"
                  value:
                    metadata:
                      title: "City-Level Cost of Living Prices (2026)"
                      license: "CC BY 4.0"
                      total_cities: 50
                    data:
                      - city_key: "PT-Lisbon"
                        city_name: "Lisbon"
                        country_code: "pt"
                        currency: "EUR"
                        item_count: 48
                        categories: 14
        "404":
          description: "City not found"
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  available:
                    type: array
                    items:
                      type: string

  /api/data/ai-comparison:
    get:
      operationId: getAiComparison
      summary: "AI-Optimized Country Comparison"
      description: |
        Structured comparison data designed for AI/LLM consumption.
        Returns JSON-LD (Schema.org Dataset) with per-dimension scores, global rankings,
        monthly cost estimates, dimension winners, natural-language summary, methodology,
        and full source attribution. Content-Type is application/ld+json.
      tags: ["AI-Optimized"]
      parameters:
        - name: a
          in: query
          required: true
          description: "First country code (ISO 3166-1 alpha-2, e.g., PT)"
          schema:
            type: string
            pattern: "^[A-Za-z]{2}$"
            example: "PT"
        - name: b
          in: query
          required: true
          description: "Second country code (ISO 3166-1 alpha-2, e.g., ES)"
          schema:
            type: string
            pattern: "^[A-Za-z]{2}$"
            example: "ES"
      responses:
        "200":
          description: "Structured comparison with JSON-LD markup"
          content:
            application/ld+json:
              schema:
                $ref: "#/components/schemas/AiComparisonResponse"
              example:
                "@context": "https://schema.org"
                "@type": "Dataset"
                name: "Portugal vs Spain: Relocation Comparison Data (2026)"
                license: "https://creativecommons.org/licenses/by/4.0/"
                comparison:
                  countryA:
                    name: "Portugal"
                    code: "PT"
                    monthlyEstimateUsd: 1680
                    overallScore: 68
                    globalRank: 12
                    scores:
                      cost: 72
                      safety: 78
                      healthcare: 65
                  countryB:
                    name: "Spain"
                    code: "ES"
                    monthlyEstimateUsd: 2100
                    overallScore: 71
                    globalRank: 8
                  dimensionWins:
                    PT: 3
                    ES: 3
                    tied: 1
                  dimensions:
                    - name: "Cost of Living"
                      dimension: "cost"
                      winner: "PT"
                      scoreA: 72
                      scoreB: 58
                      insight: "Portugal is approximately 19% more affordable for expats."
                  summary: "Spain scores higher overall (71 vs 68). Portugal has advantages in cost of living."
                sources: ["World Bank", "OECD", "WHO"]
        "400":
          description: "Missing or invalid parameters"
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  example:
                    type: string
        "404":
          description: "Country code not found"
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                  available:
                    type: array
                    items:
                      type: string

  /api/data/school-intelligence/{city}:
    get:
      operationId: getSchoolIntelligence
      summary: "Per-City School Cost Intelligence"
      description: |
        Deep intelligence report for a specific city's international schools.
        Includes stated tuition vs true annual cost (with hidden fee analysis), budget tiers,
        best-value schools, most affordable schools, curriculum distribution, regional peer
        comparison, global median comparison, and a natural-language executive summary.
        Returns JSON-LD with Schema.org Dataset markup.
      tags: ["Schools", "AI-Optimized"]
      parameters:
        - name: city
          in: path
          required: true
          description: "City slug (e.g., dubai, lisbon, bangkok, ho-chi-minh)"
          schema:
            type: string
            example: "dubai"
      responses:
        "200":
          description: "School intelligence report for the city"
          content:
            application/json:
              schema:
                type: object
                properties:
                  jsonLd:
                    type: object
                    description: "Schema.org Dataset markup"
                  city:
                    type: object
                    properties:
                      name:
                        type: string
                      slug:
                        type: string
                      country:
                        type: string
                      country_code:
                        type: string
                      region:
                        type: string
                      total_schools:
                        type: integer
                      browse_url:
                        type: string
                        format: uri
                  cost_overview:
                    type: object
                    properties:
                      median_stated_tuition_usd:
                        type: integer
                      median_true_annual_cost_usd:
                        type: integer
                      median_hidden_fee_pct:
                        type: integer
                      cheapest_true_cost_usd:
                        type: integer
                        nullable: true
                      most_expensive_true_cost_usd:
                        type: integer
                        nullable: true
                  budget_tiers:
                    type: array
                    items:
                      type: object
                      properties:
                        budget_tier:
                          type: string
                        school_count:
                          type: integer
                  best_value_schools:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        slug:
                          type: string
                        value_score:
                          type: number
                          nullable: true
                        true_annual_cost_usd:
                          type: integer
                          nullable: true
                        curriculum:
                          type: array
                          items:
                            type: string
                        browse_url:
                          type: string
                          format: uri
                  most_affordable_schools:
                    type: array
                    items:
                      type: object
                      properties:
                        name:
                          type: string
                        slug:
                          type: string
                        true_annual_cost_usd:
                          type: integer
                        curriculum:
                          type: array
                          items:
                            type: string
                        browse_url:
                          type: string
                          format: uri
                  curriculum_distribution:
                    type: array
                    items:
                      type: object
                      properties:
                        curriculum:
                          type: string
                        school_count:
                          type: integer
                        percentage:
                          type: integer
                  comparison:
                    type: object
                    properties:
                      vs_global:
                        type: object
                        nullable: true
                        properties:
                          global_median_true_cost_usd:
                            type: integer
                          city_vs_global_pct:
                            type: integer
                          label:
                            type: string
                      regional_peers:
                        type: array
                        items:
                          type: object
                          properties:
                            city:
                              type: string
                            country:
                              type: string
                            median_true_cost_usd:
                              type: integer
                            school_count:
                              type: integer
                  executive_summary:
                    type: string
                    description: "Natural-language summary of the city's school landscape"
                  metadata:
                    type: object
                    properties:
                      source:
                        type: string
                      license:
                        type: string
                      updated:
                        type: string
                        format: date
                      methodology:
                        type: string
                        format: uri
                      cite_as:
                        type: string
              example:
                city:
                  name: "Dubai"
                  slug: "dubai"
                  country: "United Arab Emirates"
                  country_code: "AE"
                  total_schools: 180
                cost_overview:
                  median_stated_tuition_usd: 12000
                  median_true_annual_cost_usd: 14500
                  median_hidden_fee_pct: 21
                executive_summary: "International schools in Dubai cost a median $12,000/year in stated tuition but the true annual cost including registration, assessment, transport, and activity fees averages $14,500 -- a 21% hidden fee markup."
        "404":
          description: "City not found"
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string

  /api/data/ai-cost-of-living/{code}:
    get:
      operationId: getAiCostOfLiving
      summary: "AI-Optimized Cost of Living by Country"
      description: |
        Structured cost of living data for a single country, optimized for AI/LLM consumption.
        Returns country-level cost breakdown with city comparisons, budget estimates,
        and source attribution. Designed for AI assistants answering "how much does it cost to live in X?"
      tags: ["AI-Optimized", "Cost of Living"]
      parameters:
        - name: code
          in: path
          required: true
          description: "ISO 3166-1 alpha-2 country code (e.g., PT, ES, TH)"
          schema:
            type: string
            pattern: "^[A-Za-z]{2}$"
            example: "PT"
      responses:
        "200":
          description: "Cost of living data for the specified country"
          content:
            application/json:
              schema:
                type: object
                properties:
                  country:
                    type: string
                  country_code:
                    type: string
                  cost_index:
                    type: number
                  monthly_estimate_usd:
                    type: number
                  sub_indexes:
                    type: object
                    properties:
                      grocery:
                        type: number
                      rent:
                        type: number
                      utilities:
                        type: number
                      transport:
                        type: number
                  license:
                    type: string
                    example: "CC BY 4.0"
        "404":
          description: "Country not found"

  /api/data/ai-tax/{code}:
    get:
      operationId: getAiTax
      summary: "AI-Optimized Tax Data by Country"
      description: |
        Tax rate calculations for a single country at a specified income level.
        Optimized for AI/LLM consumption. Returns effective tax rate, income tax,
        social contributions, and any applicable special regime information.
      tags: ["AI-Optimized", "Tax"]
      parameters:
        - name: code
          in: path
          required: true
          description: "ISO 3166-1 alpha-2 country code (e.g., PT, ES, AE)"
          schema:
            type: string
            pattern: "^[A-Za-z]{2}$"
            example: "PT"
        - name: income
          in: query
          required: false
          description: "Annual gross income in USD (default: 100000)"
          schema:
            type: integer
            example: 100000
      responses:
        "200":
          description: "Tax data for the specified country and income"
          content:
            application/json:
              schema:
                type: object
                properties:
                  country:
                    type: string
                  country_code:
                    type: string
                  gross_income:
                    type: integer
                  effective_rate:
                    type: number
                  income_tax:
                    type: number
                  social_contributions:
                    type: number
                  total_tax:
                    type: number
                  special_regimes:
                    type: array
                    items:
                      type: string
                  license:
                    type: string
                    example: "CC BY 4.0"
        "404":
          description: "Country not found"

  /api/data/ai-visa:
    get:
      operationId: getAiVisa
      summary: "AI-Optimized Visa Requirements"
      description: |
        Visa requirements for a specific passport-destination pair.
        Optimized for AI/LLM consumption. Returns access level, stay duration,
        work rights, digital nomad visa availability, and path to residency.
      tags: ["AI-Optimized", "Visa"]
      parameters:
        - name: passport
          in: query
          required: true
          description: "Passport country code (ISO 3166-1 alpha-2)"
          schema:
            type: string
            pattern: "^[A-Za-z]{2}$"
            example: "US"
        - name: destination
          in: query
          required: true
          description: "Destination country code (ISO 3166-1 alpha-2)"
          schema:
            type: string
            pattern: "^[A-Za-z]{2}$"
            example: "PT"
      responses:
        "200":
          description: "Visa requirements for the passport-destination pair"
          content:
            application/json:
              schema:
                type: object
                properties:
                  passport:
                    type: string
                  destination:
                    type: string
                  access_level:
                    type: string
                    enum: ["freedom-of-movement", "visa-free", "visa-on-arrival", "e-visa", "visa-required"]
                  max_stay_days:
                    type: integer
                    nullable: true
                  work_allowed:
                    type: boolean
                  digital_nomad_visa:
                    type: boolean
                  path_to_residency:
                    type: boolean
                  notes:
                    type: string
                    nullable: true
                  license:
                    type: string
                    example: "CC BY 4.0"
        "400":
          description: "Missing parameters"
        "404":
          description: "Passport or destination not found"

  /api/data/product-feed:
    get:
      operationId: getProductFeed
      summary: "Google Shopping Product Feed"
      description: |
        Google Merchant Center product feed in RSS 2.0 / Google Shopping format.
        Lists all premium WhereNext report products (excluding bundles) for Google Shopping surfaces.
        Content-Type is application/xml.
      tags: ["Product Feed"]
      responses:
        "200":
          description: "RSS 2.0 product feed with Google Shopping namespace"
          content:
            application/xml:
              schema:
                type: string
              example: |
                <?xml version="1.0" encoding="UTF-8"?>
                <rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">
                  <channel>
                    <title>WhereNext Personalized Plans</title>
                    <item>
                      <g:id>wherenext-decision-brief</g:id>
                      <title>Relocation Decision Plan</title>
                      <g:price>29.00 USD</g:price>
                      <g:availability>in_stock</g:availability>
                    </item>
                  </channel>
                </rss>

  /api/data/chinese-outbound-relocation-index:
    get:
      operationId: getChineseOutboundRelocationIndex
      summary: "Chinese Outbound Relocation Index 2026"
      description: |
        Ranking of destinations for mainland Chinese and Hong Kong emigrants.
        Weighted for Chinese-origin priorities (education 90, safety 85, healthcare 80, infrastructure 80) and annotated with 2025-2026 visa/policy flags (MM2H relaunch, LTR ease, UAE Golden Visa, Japan Business Manager reform, Singapore GIP S$50M, Canada/Australia foreign-buyer bans, US EB-5 China final action September 22, 2016 per May 2026 Visa Bulletin).
        Pass ?limit=95 to retrieve the full ranked list; default returns top 30.
        License: CC BY 4.0 — free to cite with attribution to WhereNext.
      tags: ["China Outbound"]
      parameters:
        - name: limit
          in: query
          required: false
          description: "Number of top-ranked destinations to return (1-95)."
          schema:
            type: integer
            minimum: 1
            maximum: 95
            default: 30
        - name: format
          in: query
          required: false
          description: "Response format. Omit for JSON; pass 'csv' for CSV export."
          schema:
            type: string
            enum: ["csv"]
      responses:
        "200":
          description: "JSON envelope with methodology, weight profile, and ranked rows"
          content:
            application/json:
              schema:
                type: object
                properties:
                  dataset:
                    type: string
                    example: "chinese-outbound-relocation-index-2026"
                  description:
                    type: string
                  methodology:
                    type: string
                  weightProfile:
                    type: object
                  license:
                    type: string
                    example: "CC BY 4.0"
                  attribution:
                    type: string
                  source:
                    type: string
                    format: uri
                  updated:
                    type: string
                    format: date
                  limit:
                    type: integer
                  maxLimit:
                    type: integer
                  totalCountriesAnalysed:
                    type: integer
                  count:
                    type: integer
                  rows:
                    type: array
                    items:
                      type: object
                      properties:
                        rank:
                          type: integer
                        code:
                          type: string
                        name:
                          type: string
                        score:
                          type: number
                        monthlyCostUsd:
                          type: integer
                        topStrengths:
                          type: array
                          items:
                            type: string
                        visaRoute:
                          type: string
                        policy:
                          type: object
                          nullable: true

  # ── Tranche 3 (2026-05-10): agent-ready intelligence endpoints ────
  # All five endpoints share the canonical IntelligenceEnvelope shape
  # (see #/components/schemas/IntelligenceEnvelope below). Inputs are
  # category-specific. Each route lives in src/app/api/intelligence/*.

  /api/intelligence/relocation-readiness:
    post:
      summary: Compute the relocation readiness score for a case.
      description: |
        **Step 2 of the WhereNext agent workflow.**
        nl-query / shortlist → **relocation-readiness** → plan-relocation
        → advisor-packet → provider-handoff.

        Wraps the same `computeReadiness` lib that powers the human
        ReadinessScorePanel. Returns 0-100 score, band, severity-tagged
        blockers, and topNextStep — wrapped in the canonical envelope.

        Example consent trail (when this score is rendered to a human
        before any handoff):
        ```json
        {
          "consent_trail": {
            "capturedAt": "2026-05-10T13:42:11Z",
            "channel": "agent_ui",
            "disclosureShown":
              "WhereNext provides decision support, not regulated advice."
          }
        }
        ```
        Agents do NOT need consent_trail to call this endpoint — only
        when they go on to call provider-handoff.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [case]
              properties:
                case:
                  type: object
      responses:
        "200":
          description: Envelope with readiness data.
          content:
            application/ld+json:
              schema:
                $ref: "#/components/schemas/IntelligenceEnvelope"

  /api/intelligence/first-year-cost:
    post:
      summary: Estimate first-year cost for a destination + household.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [destinationCountry]
              properties:
                destinationCountry: { type: string }
                household:
                  type: object
                monthlyRentUsd: { type: number }
                movingDistanceKm: { type: number }
                schoolBudgetUsd: { type: number }
      responses:
        "200":
          description: Envelope with cost data.
          content:
            application/ld+json:
              schema:
                $ref: "#/components/schemas/IntelligenceEnvelope"
        "404":
          description: Unknown destination country.

  /api/intelligence/shortlist:
    post:
      summary: Rank countries by user-supplied dimensional weights.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [weights]
      responses:
        "200":
          description: Envelope with ranked country list.
          content:
            application/ld+json:
              schema:
                $ref: "#/components/schemas/IntelligenceEnvelope"

  /api/intelligence/compare:
    post:
      summary: Side-by-side scoring for 2-4 countries.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [countryCodes]
              properties:
                countryCodes:
                  type: array
                  items: { type: string }
                  minItems: 2
                  maxItems: 4
      responses:
        "200":
          description: Envelope with ranked comparison + per-dimension winners.
          content:
            application/ld+json:
              schema:
                $ref: "#/components/schemas/IntelligenceEnvelope"

  /api/intelligence/provider-handoff:
    post:
      summary: Consented handoff to a vetted human partner (Phase 6.2).
      description: |
        **Step 5 — terminal step — of the WhereNext agent workflow.**
        nl-query / shortlist → relocation-readiness → plan-relocation →
        advisor-packet → **provider-handoff**.

        WhereNext does **not** broker, file, or fulfil; this endpoint
        records a *consented introduction* to a human partner who runs
        the case. Any referral fee is disclosed verbatim in the
        introduction email — that disclosure copy MUST also be the
        string the agent surfaces to the user before flipping
        `consent: true`. The captured copy goes into `consent_trail`.

        **Consent contract:**
        - `consent` MUST be the literal `true`.
        - `consent_trail` MUST carry `capturedAt` (ISO timestamp the
          user clicked accept), `channel` (e.g. `agent_ui`,
          `human_form`, `voice_agent`), and the verbatim
          `disclosureShown` copy. A 400 is returned if any of the
          three is missing.
        - Server logs only carry `lead_id` + UTM + IP truncation.
          Email is never echoed in analytics events.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [category, email, consent, consent_trail]
              properties:
                category:
                  type: string
                  enum: [tax, visa, school, property, moving, advisory]
                email: { type: string, format: email }
                consent:
                  type: boolean
                  enum: [true]
                consent_trail:
                  type: object
                  required: [capturedAt, channel, disclosureShown]
                  properties:
                    capturedAt:
                      type: string
                      format: date-time
                      description: ISO timestamp the user accepted the disclosure.
                    channel:
                      type: string
                      enum: [agent_ui, human_form, voice_agent, mcp_client]
                      description: Where the consent was captured.
                    disclosureShown:
                      type: string
                      description: Verbatim disclosure text the user saw before flipping consent.
            examples:
              human:
                summary: Consent captured in the WhereNext web UI
                value:
                  category: tax
                  email: buyer@example.com
                  consent: true
                  consent_trail:
                    capturedAt: "2026-05-10T13:45:01Z"
                    channel: human_form
                    disclosureShown: "WhereNext may earn a referral fee on completed engagements; the partner runs the case. WhereNext does not file taxes, prepare visa applications, or guarantee outcomes."
              agent:
                summary: Consent captured by an MCP/AI agent UI
                value:
                  category: visa
                  email: buyer@example.com
                  consent: true
                  consent_trail:
                    capturedAt: "2026-05-10T13:51:33Z"
                    channel: agent_ui
                    disclosureShown: "Connecting you to a vetted partner. WhereNext may earn a disclosed referral fee. WhereNext does not file applications and does not guarantee visa outcomes."
      responses:
        "200":
          description: Envelope with persisted lead_id.
          content:
            application/ld+json:
              schema:
                $ref: "#/components/schemas/IntelligenceEnvelope"
        "400":
          description: Missing consent or invalid payload.
        "502":
          description: Lead persistence failed.

  /api/mcp:
    post:
      summary: MCP (Model Context Protocol) Streamable HTTP transport.
      description: |
        Tranche 7 (2026-05-10); extended 2026-05-13, 2026-06-02,
        2026-06-03, 2026-06-04 and 2026-06-05. MCP server exposing 16 tools
        to remote MCP clients. Speaks the JSON-RPC framing defined by the
        MCP TypeScript SDK.

        15 read-only tools (safe to call without confirmation):
        shortlist, compare, relocation_readiness, first_year_cost,
        advisor_packet, source_lookup, nl_query, plan_relocation,
        eligibility_verdict, claim_verify, descent_eligibility,
        tax_residency, pet_import, golden_visa_match, corridor_readiness.

        1 mutating, consent-required tool: request_provider_handoff —
        submits a partner_intros lead on the user's behalf. It requires
        an explicit consent record carrying the exact disclosure copy the
        agent showed the user, and declares readOnlyHint:false so
        well-behaved clients prompt before invoking. Fabricating consent
        is a serious safety violation; every consent_trail is audited.

        Discovery manifest: /.well-known/mcp.json. Public docs:
        /platform/mcp.

        Auth: requires `Authorization: Bearer <MCP_BEARER_TOKEN>`. If
        the env var is unset, the endpoint returns 503 mcp_disabled.

        Operator runbook: docs/operator-runbooks/mcp-server.md.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: JSON-RPC request envelope (jsonrpc, id, method, params)
      responses:
        "200":
          description: JSON-RPC reply or SSE stream (depends on transport mode).
          content:
            application/json:
              schema:
                type: object
            text/event-stream:
              schema:
                type: string
        "401":
          description: Missing or invalid bearer token.
        "503":
          description: MCP_BEARER_TOKEN is not configured.

  /api/intelligence/plan-relocation:
    post:
      summary: Pattern-4 unified relocation orchestrator (Tranche 12).
      description: |
        **Step 3 of the WhereNext agent workflow.**
        nl-query / shortlist → relocation-readiness → **plan-relocation**
        → advisor-packet → provider-handoff.

        Accepts EITHER `query` (free-form) OR `case` (structured).
        Orchestrates nl-query → shortlist → readiness + first-year-cost
        per destination → recommended advisor category. Returns one
        canonical IntelligenceEnvelope with `data.shortlist`,
        `data.perDestination`, and `data.recommendation`. Agents that
        want finer control should call `relocation-readiness` and
        `advisor-packet` directly.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                query:
                  type: string
                  minLength: 1
                  maxLength: 500
                case:
                  type: object
                  description: RelocationCaseData (see /api/intelligence/relocation-readiness for shape).
                topN:
                  type: integer
                  minimum: 1
                  maximum: 10
                  default: 3
              oneOf:
                - required: [query]
                - required: [case]
      responses:
        "200":
          description: Composite envelope.
          content:
            application/ld+json:
              schema:
                $ref: "#/components/schemas/IntelligenceEnvelope"
        "400":
          description: Missing both `query` and `case`, or malformed JSON.
        "422":
          description: Could not produce a shortlist from the inputs.

  /api/intelligence/eligibility-verdict:
    post:
      summary: Deterministic, regulator-cited visa-eligibility verdict (2026-06-02).
      description: |
        For a destination + applicant profile, reports whether the
        applicant meets each modelled visa pathway's PUBLISHED financial
        threshold (income / savings / age). Returns `qualify` only when a
        real (>0) threshold is met — a $0-minimum programme is reported as
        `near`, and skilled-worker routes are never a confident pass.
        Citations point to the named official regulator and are never
        fabricated. The response carries `Cache-Control: private, no-store`
        because it echoes the requester's own income/savings/age inputs.
        Same engine as the human `/verdict` page + share card + the MCP
        `eligibility_verdict` tool.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [destinationCountry, age]
              properties:
                destinationCountry:
                  type: string
                  minLength: 2
                  maxLength: 2
                  description: ISO 3166-1 alpha-2 destination code, e.g. PT.
                nationality:
                  type: string
                  minLength: 2
                  maxLength: 2
                  default: US
                age:
                  type: integer
                  minimum: 18
                  maximum: 100
                profession:
                  type: string
                  enum: [tech, healthcare, trades, teaching, finance, creative, retired, other]
                  default: other
                education:
                  type: string
                  enum: [none, high_school, bachelors, masters, phd]
                  default: bachelors
                annualIncomeUsd:
                  type: number
                  minimum: 0
                  default: 0
                monthlyPensionUsd:
                  type: number
                  minimum: 0
                  default: 0
                liquidSavingsUsd:
                  type: number
                  minimum: 0
                  default: 0
                hasJobOffer:
                  type: boolean
                  default: false
                familySize:
                  type: integer
                  minimum: 1
                  maximum: 12
      responses:
        "200":
          description: Verdict envelope (best-first across modelled pathways), regulator citations in `citation[]`.
          content:
            application/ld+json:
              schema:
                $ref: "#/components/schemas/IntelligenceEnvelope"
        "400":
          description: Malformed JSON or schema-invalid input.
        "404":
          description: No modelled visa pathway for the destination code.

  /api/intelligence/eligibility-ranking:
    post:
      summary: Reverse Eligibility — rank every modelled destination for one profile (2026-06-02).
      description: |
        The inverse of /api/intelligence/eligibility-verdict. For a single
        applicant profile, ranks EVERY modelled destination by the best
        achievable verdict (qualify → near → short), each cited to the named
        official regulator. Same deterministic engine; never a fabricated
        source, never a confident pass on a $0-minimum programme. The response
        carries `Cache-Control: private, no-store` because the ranking is
        derived from the requester's own income/savings/age. Human surface at
        /where-can-i-go.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [age]
              properties:
                nationality:
                  type: string
                  minLength: 2
                  maxLength: 2
                  default: US
                age:
                  type: integer
                  minimum: 18
                  maximum: 100
                profession:
                  type: string
                  enum: [tech, healthcare, trades, teaching, finance, creative, retired, other]
                  default: other
                education:
                  type: string
                  enum: [none, high_school, bachelors, masters, phd]
                  default: bachelors
                annualIncomeUsd:
                  type: number
                  minimum: 0
                  default: 0
                monthlyPensionUsd:
                  type: number
                  minimum: 0
                  default: 0
                liquidSavingsUsd:
                  type: number
                  minimum: 0
                  default: 0
                hasJobOffer:
                  type: boolean
                  default: false
                familySize:
                  type: integer
                  minimum: 1
                  maximum: 12
      responses:
        "200":
          description: Ranking envelope (data.ranked best-first + data.counts), regulator citations in `citation[]`.
          content:
            application/ld+json:
              schema:
                $ref: "#/components/schemas/IntelligenceEnvelope"
        "400":
          description: Malformed JSON or schema-invalid input.

  /api/intelligence/claim-verify:
    post:
      summary: Claim-Verification arbiter — compare a structured claim to WhereNext's cited figure (2026-06-03).
      description: |
        Compares a STRUCTURED relocation-threshold claim against WhereNext's own
        verified, regulator-cited figure. Returns AGREES / DISAGREES_WITH_FIGURE /
        PARTIALLY / UNVERIFIED / UNMODELLED — a comparison to a named, dated
        source, NEVER an absolute truth judgment. DISAGREES_WITH_FIGURE is emitted
        ONLY when a programme-specific regulator source (verified ≤60 days) backs
        the WhereNext figure; otherwise UNVERIFIED. A closed/unmodelled programme
        is UNMODELLED, never a disagreement. UNVERIFIED/UNMODELLED mean WhereNext
        has no basis to arbitrate — NOT that the claim is wrong. PUBLIC-cacheable
        (echoes no caller profile, only the derived claimedUsd). Domains:
        visa-income, golden-visa-investment, retirement-pension, and
        tax-effective-rate (RESTRICTED — never DISAGREES, since the figure is
        the standard rate excl. special expat regimes; needs an income tier +
        decimal rate).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain, destination, metric, claimedValue, unit]
              properties:
                domain:
                  type: string
                  enum: [visa-income-threshold, golden-visa-investment-threshold, retirement-income-threshold, tax-effective-rate]
                destination:
                  type: string
                  minLength: 2
                  maxLength: 2
                  description: ISO 3166-1 alpha-2 destination code.
                metric:
                  type: string
                  enum: [annual_income_usd, investment_usd, monthly_pension_usd, effective_tax_rate]
                  description: Must be the one legal metric for the domain (else 400).
                claimedValue:
                  type: number
                  exclusiveMinimum: 0
                  maximum: 100000000000
                  description: Visa domains — a USD/EUR amount. tax-effective-rate — the effective rate as a DECIMAL 0–1 (0.38 = 38%).
                unit:
                  type: string
                  enum: [USD, EUR]
                incomeTier:
                  type: integer
                  enum: [50000, 100000, 200000]
                  description: REQUIRED for tax-effective-rate — the discrete income tier.
      responses:
        "200":
          description: Verdict envelope (data.verdict + data.ourFigureUsd + data.toleranceBand + citation when a real source exists). UNMODELLED is a 200, not a 404.
          content:
            application/ld+json:
              schema:
                $ref: "#/components/schemas/IntelligenceEnvelope"
        "400":
          description: Malformed JSON, schema-invalid input, or domain↔metric mismatch.

  /api/data/entity-freshness:
    get:
      summary: Per-entity-type last-updated timestamps (Tranche 12).
      description: |
        Lets agents decide whether to re-fetch entity data before
        grounding. Mirrors the `supportedEntities` block in
        `/.well-known/wherenext-capabilities.json`.
      responses:
        "200":
          description: Freshness map.
          content:
            application/json:
              schema:
                type: object
                required: [entities, capabilitiesManifest]
                properties:
                  publisher: { type: string }
                  generatedAt: { type: string }
                  entities:
                    type: array
                    items:
                      type: object
                      required: [type, idScheme, lastModified, canonicalUrlPattern]
                      properties:
                        type: { type: string }
                        idScheme: { type: string }
                        count:
                          oneOf:
                            - { type: integer }
                            - { type: 'null' }
                        lastModified: { type: string }
                        canonicalUrlPattern: { type: string }
                        notes: { type: string }
                  capabilitiesManifest: { type: string }

  /api/intelligence/nl-query:
    post:
      summary: Natural-language relocation query (Pattern 3, Tranche 11).
      description: |
        Free-form text in, structured intent + ranked shortlist out.
        Deterministic heuristic parser maps the question to scoring-
        engine inputs; no LLM dependency. Same envelope as every other
        intelligence endpoint.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [query]
              properties:
                query:
                  type: string
                  minLength: 1
                  maxLength: 500
                topN:
                  type: integer
                  minimum: 1
                  maximum: 50
                  default: 10
      responses:
        "200":
          description: Envelope with parsed extraction + ranked shortlist.
          content:
            application/ld+json:
              schema:
                $ref: "#/components/schemas/IntelligenceEnvelope"
        "400":
          description: Empty / malformed query.

  /api/intelligence/advisor-packet:
    post:
      summary: Agent-facing handoff packet for one provider category.
      description: |
        **Step 4 of the WhereNext agent workflow.**
        nl-query / shortlist → relocation-readiness → plan-relocation →
        **advisor-packet** → provider-handoff.

        Returns the disclosure copy (referral fee, regulated-advice
        boundary, no-outcome guarantee), required fields, curated
        questions, document checklist, and (when a case is provided)
        the user's readiness score / blockers — all wrapped in the
        canonical IntelligenceEnvelope. The agent SHOULD render the
        disclosure copy verbatim, capture explicit user consent, then
        POST to `/api/intelligence/provider-handoff` with `consent: true`
        and a `consent_trail.disclosureShown` matching the rendered copy.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [category]
              properties:
                category:
                  type: string
                  enum:
                    - tax
                    - visa
                    - school
                    - property
                    - moving
                    - advisory
                    - api_access
                case:
                  type: object
                  description: Optional RelocationCaseData payload for case-aware packet.
      responses:
        "200":
          description: Envelope with the per-category packet.
          content:
            application/ld+json:
              schema:
                $ref: "#/components/schemas/IntelligenceEnvelope"
        "400":
          description: Invalid category or malformed JSON.

  /api/data/best-countries-for/{persona}/csv:
    get:
      operationId: getBestCountriesForPersonaCsv
      summary: "Persona-weighted country ranking (CSV)"
      description: CSV download of the persona-weighted country ranking.
      tags: ["Rankings"]
      parameters:
        - name: persona
          in: path
          required: true
          schema:
            type: string
            enum: [digital-nomads, retirement, families, career-relocation, entrepreneurs, low-tax]
      responses:
        "200":
          description: "CSV file download"
          content:
            text/csv:
              schema:
                type: string

  /api/data/best-countries-in/{region}/csv:
    get:
      operationId: getBestCountriesInRegionCsv
      summary: "Regional country ranking (CSV)"
      description: CSV download of the regional country ranking.
      tags: ["Rankings"]
      parameters:
        - name: region
          in: path
          required: true
          schema:
            type: string
            enum: [europe, asia, americas, africa, middle-east, oceania]
      responses:
        "200":
          description: "CSV file download"
          content:
            text/csv:
              schema:
                type: string

  /api/data/best-cities-for/{persona}/csv:
    get:
      operationId: getBestCitiesForPersonaCsv
      summary: "Persona-weighted city ranking (CSV)"
      tags: ["Rankings"]
      parameters:
        - name: persona
          in: path
          required: true
          schema:
            type: string
            enum: [digital-nomads, retirement, families, career-relocation, entrepreneurs, low-tax]
      responses:
        "200":
          description: "CSV file download"
          content:
            text/csv:
              schema:
                type: string

  /api/data/best-cities-in/{region}/csv:
    get:
      operationId: getBestCitiesInRegionCsv
      summary: "Regional city ranking (CSV)"
      tags: ["Rankings"]
      parameters:
        - name: region
          in: path
          required: true
          schema:
            type: string
            enum: [europe, asia, americas, africa, middle-east, oceania]
      responses:
        "200":
          description: "CSV file download"
          content:
            text/csv:
              schema:
                type: string

  /api/data/best-countries-under/{budget}/csv:
    get:
      operationId: getBestCountriesUnderBudgetCsv
      summary: "Countries that fit a monthly budget (CSV)"
      tags: ["Rankings"]
      parameters:
        - name: budget
          in: path
          required: true
          schema:
            type: string
            enum: ["1000", "1500", "2000", "3000", "5000"]
      responses:
        "200":
          description: "CSV file download"
          content:
            text/csv:
              schema:
                type: string

  /api/data/cheapest-cities-on/{budget}/csv:
    get:
      operationId: getCheapestCitiesOnBudgetCsv
      summary: "Cheapest cities that fit a monthly budget (CSV)"
      tags: ["Rankings"]
      parameters:
        - name: budget
          in: path
          required: true
          schema:
            type: string
            enum: ["1000", "1500", "2000", "3000", "5000"]
      responses:
        "200":
          description: "CSV file download"
          content:
            text/csv:
              schema:
                type: string

  /api/data/rankings/{dimension}/csv:
    get:
      operationId: getRankingsByDimensionCsv
      summary: "Single-dimension country ranking (CSV)"
      tags: ["Rankings"]
      parameters:
        - name: dimension
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: "CSV file download"
          content:
            text/csv:
              schema:
                type: string

  /api/cron/golden-prompt-eval:
    get:
      summary: Weekly golden-prompt eval cron (Tranche 9).
      description: |
        Tranche 9 (2026-05-10). Runs the canonical golden-prompt registry
        (src/lib/ai-agent/golden-prompts.ts) against the configured AI
        assistant adapter (OpenAI by default). Scores citation correctness
        for direct/indirect prompts and refusal correctness for negative
        prompts. Persists ai_eval_runs row, emits PostHog
        ai_grounding_eval_completed, fires Sentry alert on regression.

        Vercel Cron sends GET; this route is registered in vercel.json
        crons. Bearer-auth via `CRON_SECRET`.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Eval summary.
          content:
            application/json:
              schema:
                type: object
                required: [total, passed, failed, passRate, byCategory, byDirection, adapter]
                properties:
                  total: { type: integer }
                  passed: { type: integer }
                  failed: { type: integer }
                  passRate: { type: number }
                  byCategory: { type: object }
                  byDirection: { type: object }
                  adapter: { type: string }
                  regressionAlert: { type: boolean }
        "401":
          description: Missing/invalid CRON_SECRET bearer.
        "503":
          description: OPENAI_API_KEY env var not set.

  /api/data/openai-merchant-feed:
    get:
      summary: OpenAI agentic-commerce merchant feed.
      description: |
        Tranche 8 (2026-05-10). Merchant feed in OpenAI's agentic-
        commerce shape. Each product carries the canonical agent-payment
        disclosure (refundPolicy, regulatedAdviceBoundary, payment
        processor identity) plus a checkoutHandoff URL pre-tagged with
        agent_origin=openai for revenue attribution.
      responses:
        "200":
          description: Merchant feed JSON.
          content:
            application/json:
              schema:
                type: object
                required: [schemaVersion, products, paymentProcessor]
                properties:
                  schemaVersion: { type: string }
                  paymentProcessor: { type: string, enum: ["Stripe Inc."] }
                  publisher: { type: string }
                  products:
                    type: array
                    items:
                      type: object
                      required: [id, title, price, url, consumerDisclosure]
                      properties:
                        id: { type: string }
                        title: { type: string }
                        price:
                          type: object
                          required: [amount, currency]
                          properties:
                            amount: { type: number }
                            currency: { type: string, enum: ["USD"] }
                        url: { type: string }
                        consumerDisclosure:
                          type: object
                          required: [refundPolicy, regulatedAdviceBoundary, total]
                        checkoutHandoff:
                          type: object
                          required: [url, method]

  /api/data/relocation-products:
    get:
      summary: Agent-readable product + service feed.
      description: |
        Tranche 6 (2026-05-10). Distinct from /api/data/product-feed
        (Google Shopping schema). Models the relocation product
        universe — paid reports, action plans, advisory, partner-intro
        categories — with relocation-specific fields AI agents care
        about: deliveryFormat, refundPolicy, regulatedAdviceBoundary,
        handoff endpoints, required fields. CC BY 4.0.
      responses:
        "200":
          description: Product + service feed JSON.
          content:
            application/json:
              schema:
                type: object
                properties:
                  publisher:
                    type: string
                  url:
                    type: string
                  license:
                    type: string
                  version:
                    type: string
                  products:
                    type: array
                    items:
                      type: object
                      required: [id, name, type, priceUsd, currency, url, deliveryFormat, refundPolicy, regulatedAdviceBoundary]
                      properties:
                        id: { type: string }
                        name: { type: string }
                        type: { type: string }
                        priceUsd: { type: number }
                        currency: { type: string }
                        url: { type: string }
                        sampleUrl: { type: string, nullable: true }
                        description: { type: string }
                        deliveryFormat:
                          type: string
                          enum: [pdf, workspace, email, advisor_intro]
                        refundPolicy: { type: string }
                        regulatedAdviceBoundary: { type: string }
                  services:
                    type: array
                    items:
                      type: object
                      required: [category, label, description, handoffEndpoint, requiredFields]
                      properties:
                        category:
                          type: string
                          enum: [tax, visa, school, property, moving, advisory, api_access]
                        label: { type: string }
                        description: { type: string }
                        handoffEndpoint: { type: string }
                        requiredFields:
                          type: array
                          items: { type: string }
                  capabilitiesManifest: { type: string }
                  contact: { type: string }

  /api/answers/best-countries:
    get:
      operationId: getAnswerBestCountries
      summary: "Answer: best countries for a persona + budget"
      description: |
        Pre-synthesised "where should I move" answer packet. Ranks countries for a
        persona and optional monthly budget into a short, cited shortlist an AI
        agent can quote directly.
      tags: ["AI-Optimized"]
      parameters:
        - name: persona
          in: query
          required: false
          description: "Persona slug (retirement, digital-nomads, families, career-relocation, entrepreneurs, low-tax)"
          schema:
            type: string
        - name: budget
          in: query
          required: false
          description: "Target monthly budget in USD"
          schema:
            type: integer
        - name: limit
          in: query
          required: false
          description: "Number of countries to return (1-10)"
          schema:
            type: integer
            default: 5
            maximum: 10
      responses:
        "200":
          description: "Ranked country shortlist with rationale"
          content:
            application/json:
              schema:
                type: object

  /api/answers/compare:
    get:
      operationId: getAnswerCompare
      summary: "Answer: compare two countries head-to-head"
      description: |
        Pre-synthesised two-country comparison across cost, tax, visa and
        lifestyle, ready for an AI agent to cite.
      tags: ["AI-Optimized"]
      parameters:
        - name: a
          in: query
          required: true
          description: "First country ISO 3166-1 alpha-2 code"
          schema:
            type: string
        - name: b
          in: query
          required: true
          description: "Second country ISO 3166-1 alpha-2 code"
          schema:
            type: string
      responses:
        "200":
          description: "Two-country comparison answer"
          content:
            application/json:
              schema:
                type: object

  /api/answers/cost-of-living:
    get:
      operationId: getAnswerCostOfLiving
      summary: "Answer: cost of living for a country"
      description: |
        Pre-synthesised monthly cost-of-living answer packet for one country.
      tags: ["AI-Optimized"]
      parameters:
        - name: country
          in: query
          required: true
          description: "Country ISO 3166-1 alpha-2 code"
          schema:
            type: string
      responses:
        "200":
          description: "Cost-of-living answer"
          content:
            application/json:
              schema:
                type: object

  /api/answers/visa:
    get:
      operationId: getAnswerVisa
      summary: "Answer: visa options for a passport-destination pair"
      description: |
        Pre-synthesised visa-feasibility answer for a passport-destination pair
        (visa-free access plus digital-nomad, retirement and investment routes).
      tags: ["AI-Optimized"]
      parameters:
        - name: passport
          in: query
          required: true
          description: "Passport country ISO 3166-1 alpha-2 code"
          schema:
            type: string
        - name: destination
          in: query
          required: true
          description: "Destination country ISO 3166-1 alpha-2 code"
          schema:
            type: string
      responses:
        "200":
          description: "Visa options answer"
          content:
            application/json:
              schema:
                type: object

  /api/intelligence/golden-visa:
    get:
      operationId: getIntelligenceGoldenVisa
      summary: "Golden visa programme intelligence (filterable)"
      description: |
        Filterable residency/citizenship-by-investment programme intelligence:
        minimum investment, processing time, citizenship timeline, Schengen and
        EU-citizenship pathways. JSON by default; CSV when format=csv.
      tags: ["AI-Optimized"]
      parameters:
        - name: status
          in: query
          required: false
          description: "Programme status filter (all, active)"
          schema:
            type: string
        - name: budget_max_eur
          in: query
          required: false
          description: "Maximum minimum-investment in EUR"
          schema:
            type: integer
        - name: citizenship_max_years
          in: query
          required: false
          description: "Maximum years to citizenship"
          schema:
            type: integer
        - name: presence_max_days
          in: query
          required: false
          description: "Maximum physical-presence days per year"
          schema:
            type: integer
        - name: schengen
          in: query
          required: false
          description: "Only Schengen-access programmes"
          schema:
            type: boolean
        - name: eu_citizenship
          in: query
          required: false
          description: "Only EU-citizenship-pathway programmes"
          schema:
            type: boolean
        - name: country
          in: query
          required: false
          description: "Comma-separated ISO 3166-1 alpha-2 country codes"
          schema:
            type: string
        - name: format
          in: query
          required: false
          description: "Response format"
          schema:
            type: string
            enum: [json, csv]
      responses:
        "200":
          description: "Matching golden-visa programmes"
          content:
            application/json:
              schema:
                type: object

components:
  schemas:
    IntelligenceEnvelope:
      type: object
      description: |
        Canonical Tranche 3 envelope returned by every
        /api/intelligence/* route. Schema.org Dataset wrapper around a
        domain-specific data payload + summary + sources +
        methodology + confidence + relatedLinks.
      required:
        - "@context"
        - "@type"
        - name
        - description
        - url
        - creator
        - license
        - dateModified
        - temporalCoverage
        - isAccessibleForFree
        - entity
        - data
        - summary
        - sources
        - methodology
        - confidence
        - relatedLinks
      properties:
        "@context": { type: string, enum: ["https://schema.org"] }
        "@type": { type: string, enum: ["Dataset"] }
        name: { type: string }
        description: { type: string }
        url: { type: string, format: uri }
        creator:
          type: object
          properties:
            "@type": { type: string, enum: ["Organization"] }
            name: { type: string, enum: ["WhereNext"] }
            url: { type: string, format: uri }
        license:
          type: string
          enum: ["https://creativecommons.org/licenses/by/4.0/"]
        dateModified: { type: string }
        temporalCoverage: { type: string }
        isAccessibleForFree: { type: boolean, enum: [true] }
        entity:
          type: object
          properties:
            type:
              type: string
              enum: [country, city, comparison, school_city, visa_pair]
            code: { type: string }
            name: { type: string }
        data:
          type: object
          additionalProperties: true
        summary: { type: string }
        sources:
          type: array
          items: { type: string }
        methodology:
          type: object
          properties:
            note: { type: string }
            disclaimer: { type: string }
        confidence:
          type: object
          properties:
            level: { type: string, enum: [high, medium, low] }
            caveats:
              type: array
              items: { type: string }
        relatedLinks:
          type: object
          additionalProperties: { type: string }

    RelocationIndexMeta:
      type: object
      properties:
        title:
          type: string
        version:
          type: string
        source:
          type: string
        license:
          type: string
        updated:
          type: string
          format: date
        releases:
          type: string
          format: uri
        countries:
          type: integer
        dimensions:
          type: array
          items:
            type: string
        methodology:
          type: string
          format: uri
        report:
          type: string
          format: uri
        csv:
          type: string
          format: uri

    RelocationIndexRow:
      type: object
      properties:
        rank:
          type: integer
          description: "Global rank (1 = best)"
        country_code:
          type: string
          description: "ISO 3166-1 alpha-2 (lowercase)"
        country:
          type: string
        region:
          type: string
          enum: ["Europe", "Asia", "Americas", "Africa", "Middle East", "Oceania", "Other"]
        composite_score:
          type: integer
          description: "Overall score 0-100"
        cost:
          type: integer
          description: "Cost of living score (higher = more affordable)"
        safety:
          type: integer
        healthcare:
          type: integer
        education:
          type: integer
        career:
          type: integer
        lifestyle:
          type: integer
        infrastructure:
          type: integer
        confidence:
          type: number
          description: "Data confidence score 0-1"

    CostOfLivingMeta:
      type: object
      properties:
        title:
          type: string
        version:
          type: string
        source:
          type: string
        license:
          type: string
        updated:
          type: string
          format: date
        countries:
          type: integer
        methodology:
          type: string
          format: uri
        csv:
          type: string
          format: uri

    CostOfLivingRow:
      type: object
      properties:
        rank:
          type: integer
        country_code:
          type: string
        country:
          type: string
        region:
          type: string
        cost_index:
          type: number
          description: "Overall cost index (US = 82)"
        monthly_estimate_usd:
          type: number
          description: "Estimated monthly cost for a single person"
        grocery_index:
          type: number
        rent_index:
          type: number
        utilities_index:
          type: number
        transport_index:
          type: number

    ExpatTaxCountry:
      type: object
      properties:
        countryCode:
          type: string
        countryName:
          type: string
        rates:
          type: array
          items:
            type: object
            properties:
              grossIncome:
                type: integer
                enum: [50000, 100000, 200000]
              totalTax:
                type: number
              effectiveRate:
                type: number
                description: "Effective tax rate as decimal (e.g., 0.285 = 28.5%)"
              incomeTax:
                type: number
              socialContributions:
                type: number

    DigitalNomadVisa:
      type: object
      properties:
        countryCode:
          type: string
        countryName:
          type: string
        visaName:
          type: string
        region:
          type: string
        durationMonths:
          type: integer
        renewable:
          type: boolean
        maxStayMonths:
          type: integer
          nullable: true
        costUsd:
          type: number
        minIncomeUsdYear:
          type: number
        processingDays:
          type: integer
        taxTreatment:
          type: string
          description: "e.g., 'tax-exempt', 'taxed-if-resident', 'flat-rate'"
        taxNote:
          type: string
        keyRequirement:
          type: string
        remoteWorkAllowed:
          type: boolean

    PropertyCostCountry:
      type: object
      properties:
        countryCode:
          type: string
        countryName:
          type: string
        foreignOwnership:
          type: string
          description: "e.g., 'unrestricted', 'restricted', 'leasehold-only'"
        restrictionNote:
          type: string
          nullable: true
        closingCostMinPct:
          type: number
        closingCostMaxPct:
          type: number
        annualTaxMinPct:
          type: number
        annualTaxMaxPct:
          type: number
        annualTaxNote:
          type: string
          nullable: true
        mortgageAvailable:
          type: boolean
        mortgageLtvMinPct:
          type: number
        mortgageLtvMaxPct:
          type: number
        mortgageRateMinPct:
          type: number
        mortgageRateMaxPct:
          type: number
        avgGrossYieldPct:
          type: number
        rentalTaxRatePct:
          type: number
        rentalTaxNote:
          type: string
          nullable: true
        capitalGainsTaxRatePct:
          type: number
        capitalGainsTaxNote:
          type: string
          nullable: true
        avgPricePerSqmUsd:
          type: number
        residencyViaProperty:
          type: boolean
        residencyMinInvestmentUsd:
          type: number
          nullable: true
        residencyNote:
          type: string
          nullable: true
        currency:
          type: string
        lastVerified:
          type: string
          format: date

    VisaRequirementsPerPassport:
      type: object
      properties:
        metadata:
          type: object
          properties:
            title:
              type: string
            version:
              type: string
            source:
              type: string
            license:
              type: string
            updated:
              type: string
              format: date
            passport_code:
              type: string
            total_destinations:
              type: integer
        summary:
          type: object
          properties:
            visa_free:
              type: integer
            visa_on_arrival:
              type: integer
            e_visa:
              type: integer
            visa_required:
              type: integer
            digital_nomad_visas:
              type: integer
        data:
          type: array
          items:
            type: object
            properties:
              destination_code:
                type: string
              destination:
                type: string
              access_level:
                type: string
                enum: ["freedom-of-movement", "visa-free", "visa-on-arrival", "e-visa", "visa-required"]
              max_stay_days:
                type: integer
                nullable: true
              work_allowed:
                type: boolean
              digital_nomad_visa:
                type: boolean
              path_to_residency:
                type: boolean
              notes:
                type: string
                nullable: true

    PassportPowerIndex:
      type: object
      properties:
        metadata:
          type: object
          properties:
            title:
              type: string
            version:
              type: string
            source:
              type: string
            license:
              type: string
            updated:
              type: string
              format: date
            total_passports:
              type: integer
        data:
          type: array
          items:
            type: object
            properties:
              passport_code:
                type: string
              country:
                type: string
              visa_free_destinations:
                type: integer
              total_destinations:
                type: integer
              passport_power_rank:
                type: integer

    SchoolCostsAllCities:
      type: object
      properties:
        metadata:
          type: object
          properties:
            title:
              type: string
            version:
              type: string
            source:
              type: string
            license:
              type: string
            updated:
              type: string
              format: date
            total_cities:
              type: integer
            total_schools:
              type: integer
        data:
          type: array
          items:
            type: object
            properties:
              city:
                type: string
              city_name:
                type: string
              country:
                type: string
              country_code:
                type: string
              total_schools:
                type: integer
              tuition_min_usd:
                type: number
                nullable: true
              tuition_max_usd:
                type: number
                nullable: true
              tuition_median_usd:
                type: number
                nullable: true
              curricula:
                type: array
                items:
                  type: string

    SchoolCostsSingleCity:
      type: object
      properties:
        metadata:
          type: object
          properties:
            title:
              type: string
            version:
              type: string
            source:
              type: string
            license:
              type: string
            updated:
              type: string
              format: date
            city:
              type: string
            country_code:
              type: string
            disclaimer:
              type: string
        summary:
          type: object
          properties:
            total_schools:
              type: integer
            curricula:
              type: array
              items:
                type: string
            tuition_range_usd:
              type: object
              nullable: true
              properties:
                min:
                  type: number
                max:
                  type: number
                median:
                  type: number
            with_learning_support:
              type: integer

    SchoolCostIndexCity:
      type: object
      properties:
        rank:
          type: integer
        city_slug:
          type: string
        city_name:
          type: string
        country:
          type: string
        country_code:
          type: string
        school_count:
          type: integer
        median_tuition_usd:
          type: integer
        p25_tuition_usd:
          type: integer
        p75_tuition_usd:
          type: integer
        p10_tuition_usd:
          type: integer
        p90_tuition_usd:
          type: integer
        curricula:
          type: array
          items:
            type: string
        browse_url:
          type: string
          format: uri

    BudgetBracket:
      type: object
      properties:
        bracket:
          type: string
          description: "e.g., 'Under $5,000/year'"
        bracket_key:
          type: string
          description: "e.g., 'under_5k'"
        school_count:
          type: integer
        city_count:
          type: integer
        country_count:
          type: integer
        pct_of_total:
          type: integer

    AffordableCity:
      type: object
      properties:
        rank:
          type: integer
        city_slug:
          type: string
        city_name:
          type: string
        country:
          type: string
        country_code:
          type: string
        total_schools:
          type: integer
        schools_under_budget:
          type: integer
        coverage_pct:
          type: integer
          description: "Percentage of city's schools under the budget threshold"
        browse_url:
          type: string
          format: uri

    ClassSizeCity:
      type: object
      properties:
        rank:
          type: integer
        city_slug:
          type: string
        city_name:
          type: string
        country:
          type: string
        country_code:
          type: string
        school_count:
          type: integer
        median_class_size:
          type: integer
        smallest_class_size:
          type: integer
        largest_class_size:
          type: integer
        browse_url:
          type: string
          format: uri

    DiverseCity:
      type: object
      properties:
        rank:
          type: integer
        city_slug:
          type: string
        city_name:
          type: string
        country:
          type: string
        country_code:
          type: string
        school_count:
          type: integer
        median_nationalities:
          type: integer
        max_nationalities:
          type: integer
        median_diversity_score:
          type: integer
        browse_url:
          type: string
          format: uri

    CityPricesDetail:
      type: object
      properties:
        metadata:
          type: object
          properties:
            title:
              type: string
            version:
              type: string
            source:
              type: string
            license:
              type: string
            updated:
              type: string
              format: date
            city:
              type: string
            currency:
              type: string
            exchange_rate:
              type: number
            data_source:
              type: string
        data:
          type: array
          items:
            type: object
            properties:
              category:
                type: string
              item:
                type: string
              price_usd:
                type: number
              price_local:
                type: number
                nullable: true

    CityPricesList:
      type: object
      properties:
        metadata:
          type: object
          properties:
            title:
              type: string
            version:
              type: string
            source:
              type: string
            license:
              type: string
            updated:
              type: string
              format: date
            total_cities:
              type: integer
        data:
          type: array
          items:
            type: object
            properties:
              city_key:
                type: string
              city_name:
                type: string
              country_code:
                type: string
              currency:
                type: string
              item_count:
                type: integer
              categories:
                type: integer

    AiComparisonResponse:
      type: object
      properties:
        "@context":
          type: string
          const: "https://schema.org"
        "@type":
          type: string
          const: "Dataset"
        name:
          type: string
        description:
          type: string
        license:
          type: string
          format: uri
        dateModified:
          type: string
          format: date
        isAccessibleForFree:
          type: boolean
        comparison:
          type: object
          properties:
            countryA:
              $ref: "#/components/schemas/ComparisonCountry"
            countryB:
              $ref: "#/components/schemas/ComparisonCountry"
            dimensionWins:
              type: object
              additionalProperties:
                type: integer
            dimensions:
              type: array
              items:
                type: object
                properties:
                  name:
                    type: string
                  dimension:
                    type: string
                    enum: ["cost", "safety", "healthcare", "education", "career", "lifestyle", "infrastructure"]
                  winner:
                    type: string
                    nullable: true
                    description: "Country code of winner, or null if tied"
                  scoreA:
                    type: integer
                  scoreB:
                    type: integer
                  insight:
                    type: string
                    description: "Natural-language comparison sentence"
            summary:
              type: string
              description: "Natural-language overall summary"
        methodology:
          type: object
          properties:
            scoringWeights:
              type: object
            note:
              type: string
        sources:
          type: array
          items:
            type: string
        relatedLinks:
          type: object
          properties:
            fullComparison:
              type: string
              format: uri
            countryA:
              type: string
              format: uri
            countryB:
              type: string
              format: uri
            allComparisons:
              type: string
              format: uri
            apiDocs:
              type: string
              format: uri

    ComparisonCountry:
      type: object
      properties:
        name:
          type: string
        code:
          type: string
        monthlyEstimateUsd:
          type: number
        overallScore:
          type: integer
        globalRank:
          type: integer
        globalRankOutOf:
          type: integer
        scores:
          type: object
          properties:
            cost:
              type: integer
            safety:
              type: integer
            healthcare:
              type: integer
            education:
              type: integer
            career:
              type: integer
            lifestyle:
              type: integer
            infrastructure:
              type: integer
        confidenceScore:
          type: number
