openapi: "3.1.0"

info:
  title: MatrixAPI
  version: "1"
  description: |
    **MatrixAPI** provides high-performance, immutable reference data for developers.
    Free endpoints use aggressive server-side caching (1-year TTL) and return
    `Cache-Control: max-age=31556952, public, immutable` headers for CDN/browser caching.
    Key-authenticated endpoints return `Cache-Control: private, max-age=900` (15 minutes)
    so responses never land in shared caches.

    ## Authentication
    Most endpoints are **free and public** — no key required.
    **Vehicles**, **Airports**, and **Events** require an `X-API-Key` header.
    [Sign up to get your key.](https://matrixapi.dev/users/sign_up)

    ## Rate Limiting
    All endpoints are limited to **120 requests per minute** — per API key on
    authenticated endpoints, per IP on free endpoints. Exceeding the limit returns
    `429 Too Many Requests` with `{ "error", "message", "retry_after" }`.

    ## Response Modes
    Most list endpoints support two modes:
    - **Minimal (default)** — optimized for dropdowns and UI components
    - **Comprehensive (`?all=true`)** — all available fields

    ## Async Endpoints
    Some cities endpoints return `202 Accepted` while AI-powered city population
    runs in the background. Poll after `retry_after` seconds.

    ## Error Responses
    Errors use one of two schemas depending on the endpoint group:
    - **Flat**: `{ "error": "string" }` — Vehicles, Airports, Events
    - **Structured**: `{ "error": { "code": "not_found", "message": "...", ... } }` — Countries, Regions, Subregions, States, Cities, Currencies

servers:
  - url: https://matrixapi.dev/api/v1
    description: Production

tags:
  - name: Status
    description: Service health and API discovery
  - name: Vehicles
    description: |
      Vehicle makes and models sourced from NHTSA vPIC (1981–2026).
      **Authentication required.**
  - name: Countries
    description: |
      ISO country codes, geographic metadata, and economic indicators for 250 countries.
      Free and public.
  - name: Subdivisions
    description: |
      States, provinces, departments, and other administrative subdivisions.
      Accessed via `/countries/{code}/subdivisions` or the standalone `/states` endpoint.
      Free and public.
  - name: Cities
    description: |
      Cities within subdivisions. Supports human-readable case-insensitive names.
      Some endpoints trigger async AI population on first access.
      Free and public.
  - name: Regions
    description: 6 world regions (Africa, Americas, Asia, Europe, Oceania, Polar). Free and public.
  - name: Subregions
    description: 22 geographic subregions within the 6 world regions. Free and public.
  - name: States
    description: |
      Standalone subdivisions endpoint — alias for the subdivisions data.
      Useful for cross-country queries without a country code prefix.
      Free and public.
  - name: Currencies
    description: ISO 4217 static currency reference data. No live exchange rates. Free and public.
  - name: Airports
    description: |
      Global airport database — 67,000+ airports with IATA/ICAO codes, GPS coordinates,
      elevation, and type classification. Paginated (100/page).
      **Authentication required.**
  - name: Events
    description: |
      Local community events across the US, continuously updated from local
      news sources. Search by location, date, category, or keyword.
      **Authentication required.**

components:
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        Required for Vehicles, Airports, and Events endpoints. [Get a key.](https://matrixapi.dev/users/sign_up)
        Also accepted as an `api_key` query parameter.

        All endpoints are rate-limited to **120 requests per minute** (per key on
        authenticated endpoints, per IP on free endpoints). Exceeding the limit
        returns `429 Too Many Requests` with a `retry_after` value in seconds.

  responses:
    RateLimited:
      description: Rate limit exceeded — maximum 120 requests per minute per API key (per IP on free endpoints)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimitError'

  schemas:
    # ── Shared ──────────────────────────────────────────────
    CacheInfo:
      type: object
      properties:
        cache_duration:
          type: string
          enum: [immutable, no-cache]
          example: immutable

    AsyncPending:
      type: object
      description: Returned as HTTP 202 when city data is still being populated by a background job.
      required: [status, message, retry_after]
      properties:
        status:
          type: string
          enum: [pending]
          example: pending
        message:
          type: string
          example: "City data is being populated. Retry in a few seconds."
        retry_after:
          type: integer
          description: Seconds to wait before retrying
          example: 5
        endpoint:
          type: string
          example: "/api/v1/countries/XK/subdivisions/XUF/cities"

    FlatError:
      type: object
      description: Simple error response used by Vehicles, Airports, and Events.
      required: [error]
      properties:
        error:
          type: string
          example: "Vehicle make 'invalid-make' not found"
        suggestions:
          type: array
          items:
            type: string
          description: Up to 5 similarly named makes — present on vehicle make 404s
        hint:
          type: string
          description: Present on vehicle make 404s
          example: "GET /api/v1/vehicles lists all available makes"

    StructuredError:
      type: object
      description: |
        Structured error response used by Countries, Regions, Subregions, States,
        Cities, and Currencies endpoints. `resource` and `identifier` are present
        on Countries 404s; `available_count` on Currency 404s.
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum: [not_found]
              example: not_found
            message:
              type: string
              example: "Country not found"
            resource:
              type: string
              example: country
            identifier:
              type: string
              example: "invalid-code"
            available_count:
              type: integer
              description: Number of known currencies — present on Currency 404s
              example: 155

    RateLimitError:
      type: object
      description: Returned as HTTP 429 when the 120 requests/minute limit is exceeded.
      required: [error, message, retry_after]
      properties:
        error:
          type: string
          example: "Rate limit exceeded"
        message:
          type: string
          example: "Maximum 120 requests per minute. Slow down and retry shortly."
        retry_after:
          type: integer
          description: Seconds to wait before retrying
          example: 60

    Pagination:
      type: object
      properties:
        current_page:
          type: integer
          example: 1
        per_page:
          type: integer
          example: 100
        total_pages:
          type: integer
          example: 7
        next_page:
          type: integer
          nullable: true
          example: 2
        prev_page:
          type: integer
          nullable: true
          example: null

    # ── Events ───────────────────────────────────────────────
    EventSummary:
      type: object
      properties:
        id: { type: integer, example: 4821 }
        title: { type: string, example: "Downtown Frederick Farmers Market" }
        starts_at: { type: string, format: date-time, example: "2026-05-09T08:00:00Z" }
        ends_at: { type: string, format: date-time, nullable: true, example: "2026-05-09T13:00:00Z" }
        all_day: { type: boolean, example: false }
        venue_name: { type: string, nullable: true, example: "Carroll Creek Park" }
        city: { type: string, example: "Frederick" }
        state: { type: string, example: "MD" }
        category: { type: string, nullable: true, example: "community" }
        price_text: { type: string, nullable: true, example: "Free" }
        source_url: { type: string, format: uri, example: "https://www.fredericknewspost.com/calendar/event_abc123.html" }

    EventFull:
      type: object
      properties:
        id: { type: integer, example: 4821 }
        title: { type: string, example: "Downtown Frederick Farmers Market" }
        description: { type: string, nullable: true }
        starts_at: { type: string, format: date-time, example: "2026-05-09T08:00:00Z" }
        ends_at: { type: string, format: date-time, nullable: true }
        timezone: { type: string, nullable: true, example: "America/New_York" }
        all_day: { type: boolean, example: false }
        city: { type: string, example: "Frederick" }
        state: { type: string, example: "MD" }
        venue_name: { type: string, nullable: true }
        venue_address: { type: string, nullable: true }
        venue_lat: { type: number, format: float, nullable: true, example: 39.4143 }
        venue_lng: { type: number, format: float, nullable: true, example: -77.4105 }
        price_text: { type: string, nullable: true, example: "Free" }
        price_min: { type: number, format: float, nullable: true, example: 0 }
        price_max: { type: number, format: float, nullable: true, example: 25 }
        is_free: { type: boolean, nullable: true, example: true }
        ticket_url: { type: string, format: uri, nullable: true }
        organizer: { type: string, nullable: true }
        category: { type: string, nullable: true }
        image_urls:
          type: array
          nullable: true
          items: { type: string, format: uri }
        source_url: { type: string, format: uri }

    # ── Status ───────────────────────────────────────────────
    StatusResponse:
      type: object
      properties:
        service:
          type: string
          example: MatrixAPI
        version:
          type: string
          example: v1
        status:
          type: string
          enum: [operational, degraded, maintenance]
          example: operational
        cache_strategy:
          type: string
          example: immutable
        available_apis:
          type: object
          additionalProperties:
            type: object
            properties:
              endpoint:
                type: string
              description:
                type: string
              status:
                type: string
                enum: [available, coming_soon, deprecated]
        documentation:
          type: string
          format: uri
          example: "https://matrixapi.dev/docs"
        support:
          type: string
          example: "support@matrixapi.dev"

    # ── Vehicles ─────────────────────────────────────────────
    VehicleMinimal:
      type: object
      required: [name, endpoint]
      properties:
        name:
          type: string
          example: bmw
        endpoint:
          type: string
          example: "https://matrixapi.dev/api/v1/vehicles/2026/bmw"

    VehicleFull:
      allOf:
        - $ref: '#/components/schemas/VehicleMinimal'
        - type: object
          properties:
            full_name:
              type: string
              example: "BAYERISCHE MOTOREN WERKE AG"
            country:
              type: string
              example: "GERMANY"
            vehicle_types:
              type: array
              items: { type: string }
              example: ["Passenger Car"]
            total_models:
              type: integer
              example: 42

    VehicleIndexResponse:
      type: object
      properties:
        vehicles:
          type: array
          items:
            oneOf:
              - $ref: '#/components/schemas/VehicleMinimal'
              - $ref: '#/components/schemas/VehicleFull'
        total:
          type: integer
          example: 99
        statistics:
          type: object
          properties:
            total_makes: { type: integer, example: 2508 }
            filtered_makes: { type: integer }
            total_models: { type: integer, example: 15389 }
            total_model_years: { type: integer, example: 189249 }
            year_range: { type: string, example: "1981-2026" }
            countries_represented: { type: integer, example: 89 }
        available_countries:
          type: array
          items: { type: string }
          example: ["GERMANY", "JAPAN", "UNITED STATES (USA)"]
        available_types:
          type: array
          items: { type: string }
          example: ["Passenger Car", "Truck", "Multipurpose Passenger Vehicle (MPV)", "Motorcycle"]
        cache_duration:
          type: string
          example: immutable
        available_fields:
          type: array
          items: { type: string }

    VehicleModel:
      type: object
      properties:
        name:
          type: string
          example: "3 Series"
        available_years:
          type: array
          items: { type: integer }
          example: [2010, 2011, 2012, 2025, 2026]
        vehicle_type:
          type: string
          description: Present with `?all=true`
          example: "Passenger Car"
        nhtsa_model_id:
          type: integer
          description: Present with `?all=true`
          example: 1719

    VehicleShowResponse:
      type: object
      properties:
        make:
          type: string
          example: bmw
        year:
          type: integer
          example: 2026
        is_current_year:
          type: boolean
          description: True when no year was specified in the request (defaulted to current year)
          example: true
        models:
          type: array
          items:
            $ref: '#/components/schemas/VehicleModel'
        total:
          type: integer
          example: 42
        statistics:
          type: object
          properties:
            total_models_for_make: { type: integer }
            models_for_year: { type: integer }
            available_years: { type: array, items: { type: integer } }
            year_range: { type: string, example: "1981-2026" }
        vehicle_types:
          type: array
          items: { type: string }
          description: Distinct vehicle types among the returned models
          example: ["Passenger Car"]
        make_info:
          type: object
          properties:
            full_name: { type: string, example: "BAYERISCHE MOTOREN WERKE AG" }
            common_name: { type: string, nullable: true, example: "BMW" }
            country: { type: string, example: "GERMANY" }
            all_vehicle_types: { type: array, items: { type: string } }
        _notice:
          type: string
          description: Present on legacy /:make/:model responses to guide migration to the /:year/:make/:model pattern
        cache_duration:
          type: string
          example: immutable

    VehicleMakeModelsResponse:
      type: object
      properties:
        make: { type: string, example: bmw }
        models:
          type: array
          items:
            $ref: '#/components/schemas/VehicleModel'
        total: { type: integer, example: 128 }
        available_fields:
          type: array
          items: { type: string }
        cache_duration: { type: string, example: immutable }

    VehicleMakeYearsResponse:
      type: object
      properties:
        make: { type: string, example: "BMW" }
        years:
          type: array
          items: { type: integer }
          example: [1981, 1982, 2025, 2026]
        total: { type: integer, example: 45 }
        year_range: { type: string, example: "1981-2026" }
        cache_duration: { type: string, example: immutable }

    VehiclesByYearResponse:
      type: object
      properties:
        year: { type: integer, example: 1985 }
        makes:
          type: array
          items:
            $ref: '#/components/schemas/VehicleMinimal'
        total: { type: integer, example: 32 }
        cache_duration: { type: string, example: immutable }

    # ── Countries ─────────────────────────────────────────────
    CountryMinimal:
      type: object
      required: [name, iso2, iso3, emoji, region, endpoint]
      properties:
        name: { type: string, example: "Germany" }
        iso2: { type: string, example: "DE" }
        iso3: { type: string, example: "DEU" }
        emoji: { type: string, example: "🇩🇪" }
        region: { type: string, example: "Europe" }
        endpoint: { type: string, example: "https://matrixapi.dev/api/v1/countries/DE" }

    CountryFull:
      type: object
      properties:
        name: { type: string, example: "Germany" }
        iso2: { type: string, example: "DE" }
        iso3: { type: string, example: "DEU" }
        numeric_code: { type: string, example: "276" }
        nationality: { type: string, example: "German" }
        region: { type: string, example: "Europe" }
        subregion: { type: string, example: "Western Europe" }
        capital: { type: string, example: "Berlin" }
        currency: { type: string, example: "EUR" }
        currency_name: { type: string, example: "Euro" }
        currency_symbol: { type: string, example: "€" }
        phone_code: { type: string, example: "49" }
        tld: { type: string, example: ".de" }
        native_name: { type: string, example: "Deutschland" }
        primary_language: { type: string, example: "German" }
        main_religion: { type: string, nullable: true, example: "Christianity" }
        driving_side: { type: string, enum: [left, right], example: right }
        landlocked: { type: boolean, example: false }
        un_member: { type: boolean, example: true }
        population: { type: integer, example: 83783942 }
        density: { type: number, example: 234.2 }
        area: { type: number, example: 357114 }
        gdp_per_capita: { type: number, example: 46208 }
        median_age: { type: number, example: 45.7 }
        website: { type: string, nullable: true, example: "https://www.deutschland.de" }
        subdivision_type: { type: string, example: "states" }
        total_subdivisions: { type: integer, example: 16 }
        subdivisions_endpoint: { type: string }
        latitude: { type: string, example: "51.0" }
        longitude: { type: string, example: "9.0" }
        timezones:
          type: array
          items:
            type: object
            properties:
              zoneName: { type: string }
              gmtOffset: { type: integer }
              gmtOffsetName: { type: string }
        translations:
          type: object
          additionalProperties: { type: string }
          example: { de: "Deutschland", fr: "Allemagne" }
        emoji: { type: string, example: "🇩🇪" }
        emoji_u: { type: string, example: "U+1F1E9 U+1F1EA" }
        endpoint: { type: string }

    CountryIndexResponse:
      type: object
      properties:
        countries:
          type: array
          items:
            oneOf:
              - $ref: '#/components/schemas/CountryMinimal'
              - $ref: '#/components/schemas/CountryFull'
        total: { type: integer, example: 250 }
        statistics:
          type: object
          properties:
            total_countries: { type: integer, example: 250 }
            filtered_countries: { type: integer }
            total_regions: { type: integer, example: 6 }
            total_subregions: { type: integer, example: 22 }
        available_regions:
          type: array
          items: { type: string }
          example: ["Africa", "Americas", "Asia", "Europe", "Oceania", "Polar"]
        available_subregions:
          type: array
          items: { type: string }
        cache_duration: { type: string, example: immutable }
        available_fields:
          type: array
          items: { type: string }

    CountryShowResponse:
      type: object
      properties:
        country:
          $ref: '#/components/schemas/CountryFull'
        navigation:
          type: object
          properties:
            subdivisions: { type: string, nullable: true }
            cities: { type: string }
            all_countries: { type: string }
        statistics:
          type: object
          properties:
            subdivision_count: { type: integer, example: 16 }
            has_subdivisions: { type: boolean, example: true }
        available_fields:
          type: array
          items: { type: string }
        cache_duration: { type: string, example: immutable }

    # ── Subdivisions ─────────────────────────────────────────
    Subdivision:
      type: object
      properties:
        name: { type: string, example: "Bavaria" }
        code: { type: string, example: "BY" }
        type: { type: string, example: "state" }
        latitude: { type: string, example: "48.7904" }
        longitude: { type: string, example: "11.4979" }
        cities_count: { type: integer, example: 247 }
        endpoint: { type: string, example: "https://matrixapi.dev/api/v1/countries/germany/bavaria" }

    SubdivisionsResponse:
      type: object
      properties:
        subdivisions:
          type: array
          items:
            $ref: '#/components/schemas/Subdivision'
        total: { type: integer, example: 16 }
        country:
          type: object
          properties:
            name: { type: string }
            iso2: { type: string }
            endpoint: { type: string }
        subdivision_type: { type: string, example: "states" }
        statistics:
          type: object
          properties:
            total_subdivisions: { type: integer }
            subdivisions_with_cities: { type: integer }
        cache_duration: { type: string, example: immutable }

    SubdivisionDetailsResponse:
      type: object
      properties:
        subdivision:
          type: object
          properties:
            name: { type: string, example: "Ferizaj" }
            code: { type: string, example: "XUF" }
            subdivision_type: { type: string, example: "districts" }
            type: { type: string, nullable: true, example: "district" }
            latitude: { type: string, nullable: true }
            longitude: { type: string, nullable: true }
            cities_count: { type: integer, example: 7 }
            endpoint: { type: string, example: "https://matrixapi.dev/api/v1/countries/kosovo/ferizaj" }
        country:
          $ref: '#/components/schemas/CountryLink'
        navigation:
          type: object
          properties:
            country_subdivisions: { type: string }
            subdivision_cities: { type: string, nullable: true }
            all_countries: { type: string }
        statistics:
          type: object
          properties:
            cities_count: { type: integer, example: 7 }
            has_cities: { type: boolean, example: true }
        canonical_url: { type: string, example: "https://matrixapi.dev/api/v1/countries/xk/subdivisions/xuf" }
        url_note: { type: string }
        available_fields:
          type: array
          items: { type: string }
        cache_duration: { type: string, example: immutable }

    CountryLink:
      type: object
      properties:
        name: { type: string, example: "Kosovo" }
        iso2: { type: string, example: "XK" }
        endpoint: { type: string, example: "https://matrixapi.dev/api/v1/countries/xk" }

    SubdivisionLink:
      type: object
      properties:
        name: { type: string, example: "Ferizaj" }
        code: { type: string, example: "XUF" }
        subdivision_type: { type: string, example: "districts" }
        endpoint: { type: string, example: "https://matrixapi.dev/api/v1/countries/kosovo/ferizaj" }

    # ── Cities ───────────────────────────────────────────────
    City:
      type: object
      properties:
        id: { type: integer, example: 157206 }
        name: { type: string, example: "Ferizaj" }
        latitude: { type: string, example: "42.3708" }
        longitude: { type: string, example: "21.1531" }
        endpoint: { type: string }

    CityFull:
      allOf:
        - $ref: '#/components/schemas/City'
        - type: object
          properties:
            state_code: { type: string, example: "XUF" }
            country_iso2: { type: string, example: "XK" }
            state_name: { type: string, example: "Ferizaj" }
            country_name: { type: string, example: "Kosovo" }

    CitiesResponse:
      type: object
      description: Response of the canonical `/countries/{code}/subdivisions/{subdivision_code}/cities` endpoint.
      properties:
        country: { type: string, example: "Kosovo" }
        subdivision: { type: string, example: "Ferizaj" }
        subdivision_code: { type: string, example: "XUF" }
        subdivision_type: { type: string, example: "districts" }
        cities:
          type: array
          items:
            type: object
            properties:
              id: { type: integer, example: 157206 }
              name: { type: string, example: "Ferizaj" }
              latitude: { type: string, nullable: true, example: "42.3708" }
              longitude: { type: string, nullable: true, example: "21.1531" }
        total: { type: integer }
        data_source: { type: string, enum: [database], example: database }
        cache_duration: { type: string, example: immutable }

    CitiesHumanReadableResponse:
      type: object
      description: Response of the human-readable `/countries/{name}/{subdivision}/cities` endpoint.
      properties:
        cities:
          type: array
          items:
            $ref: '#/components/schemas/City'
        total: { type: integer }
        subdivision:
          $ref: '#/components/schemas/SubdivisionLink'
        country:
          $ref: '#/components/schemas/CountryLink'
        navigation:
          type: object
          properties:
            subdivision_details: { type: string }
            country_subdivisions: { type: string }
            all_countries: { type: string }
        statistics:
          type: object
          properties:
            total_cities: { type: integer }
            data_source: { type: string, example: database }
        canonical_url: { type: string, example: "https://matrixapi.dev/api/v1/countries/xk/subdivisions/xuf/cities" }
        url_note: { type: string }
        available_fields:
          type: array
          items: { type: string }
        cache_duration: { type: string, example: immutable }

    CountryCitiesResponse:
      type: object
      properties:
        cities:
          type: array
          items:
            type: object
            properties:
              name: { type: string, example: "Shtime" }
              state: { type: string, nullable: true, example: "Ferizaj" }
              state_code: { type: string, nullable: true, example: "XUF" }
              latitude: { type: string, nullable: true }
              longitude: { type: string, nullable: true }
        total: { type: integer }
        country:
          $ref: '#/components/schemas/CountryLink'
        available_fields:
          type: array
          items: { type: string }
        cache_duration: { type: string, example: immutable }

    CityIndexResponse:
      type: object
      properties:
        cities:
          type: array
          items:
            oneOf:
              - $ref: '#/components/schemas/City'
              - $ref: '#/components/schemas/CityFull'
        total:
          type: integer
          description: Global city count — see `filtered_total` for the filtered count
        filtered_total: { type: integer }
        _notice:
          type: string
          nullable: true
          description: Present when results were truncated to 1000
        statistics:
          type: object
          properties:
            total_cities: { type: integer, example: 150000 }
            filtered_cities: { type: integer }
            countries_with_cities: { type: integer, example: 250 }
            states_with_cities: { type: integer }
        available_fields:
          type: array
          items: { type: string }
        cache_duration: { type: string, example: immutable }

    CityShowResponse:
      type: object
      properties:
        id: { type: integer, example: 157206 }
        name: { type: string, example: "Shtime" }
        state:
          type: object
          properties:
            code: { type: string, nullable: true, example: "XUF" }
            name: { type: string, nullable: true, example: "Ferizaj" }
            type: { type: string, nullable: true, example: "district" }
        country:
          type: object
          properties:
            iso2: { type: string, example: "XK" }
            iso3: { type: string, example: "XKX" }
            name: { type: string, example: "Kosovo" }
        coordinates:
          type: object
          properties:
            latitude: { type: string, nullable: true }
            longitude: { type: string, nullable: true }
        cache_duration: { type: string, example: immutable }

    CityDetailsResponse:
      type: object
      properties:
        city:
          type: object
          properties:
            id: { type: integer, example: 157206 }
            name: { type: string, example: "Shtime" }
            latitude: { type: string, nullable: true }
            longitude: { type: string, nullable: true }
            endpoint: { type: string, example: "https://matrixapi.dev/api/v1/countries/kosovo/ferizaj/shtime" }
        subdivision:
          $ref: '#/components/schemas/SubdivisionLink'
        country:
          $ref: '#/components/schemas/CountryLink'
        navigation:
          type: object
          properties:
            all_cities: { type: string }
            subdivision_details: { type: string }
            country_subdivisions: { type: string }
            all_countries: { type: string }
        canonical_url: { type: string, example: "https://matrixapi.dev/api/v1/countries/xk/cities?state=xuf" }
        available_fields:
          type: array
          items: { type: string }
        cache_duration: { type: string, example: immutable }

    # ── Regions ──────────────────────────────────────────────
    Region:
      type: object
      properties:
        name: { type: string, example: "Europe" }
        subregion_count: { type: integer, example: 4 }
        country_count: { type: integer, example: 51 }

    RegionShowResponse:
      allOf:
        - $ref: '#/components/schemas/Region'
        - type: object
          properties:
            subregions:
              type: array
              items:
                type: object
                properties:
                  name: { type: string }
            countries:
              type: array
              items:
                type: object
                properties:
                  name: { type: string }
                  iso2: { type: string }
            cache_duration: { type: string, example: immutable }

    RegionsIndexResponse:
      type: object
      properties:
        regions:
          type: array
          items:
            $ref: '#/components/schemas/Region'
        total: { type: integer, example: 6 }
        statistics:
          type: object
          properties:
            total_regions: { type: integer, example: 6 }
            total_subregions: { type: integer, example: 22 }
            total_countries: { type: integer, example: 250 }
        cache_duration: { type: string, example: immutable }

    # ── Subregions ───────────────────────────────────────────
    Subregion:
      type: object
      properties:
        name: { type: string, example: "Western Europe" }
        region: { type: string, example: "Europe" }
        country_count: { type: integer, example: 9 }

    SubregionShowResponse:
      allOf:
        - $ref: '#/components/schemas/Subregion'
        - type: object
          properties:
            countries:
              type: array
              items:
                type: object
                properties:
                  name: { type: string }
                  iso2: { type: string }
            cache_duration: { type: string, example: immutable }

    SubregionsIndexResponse:
      type: object
      properties:
        subregions:
          type: array
          items:
            $ref: '#/components/schemas/Subregion'
        total: { type: integer, example: 22 }
        statistics:
          type: object
          properties:
            total_subregions: { type: integer, example: 22 }
            total_regions: { type: integer, example: 6 }
            total_countries: { type: integer, example: 250 }
        available_regions:
          type: array
          items: { type: string }
          example: ["Africa", "Americas", "Asia", "Europe", "Oceania", "Polar"]
        cache_duration: { type: string, example: immutable }

    # ── States (standalone subdivisions) ─────────────────────
    StateMinimal:
      type: object
      properties:
        name: { type: string, example: "California" }
        code: { type: string, example: "CA" }
        country: { type: string, example: "US" }

    StateFull:
      allOf:
        - $ref: '#/components/schemas/StateMinimal'
        - type: object
          properties:
            type: { type: string, example: "state" }
            latitude: { type: string, example: "36.7783" }
            longitude: { type: string, example: "-119.4179" }

    StatesIndexResponse:
      type: object
      properties:
        states:
          type: array
          items:
            oneOf:
              - $ref: '#/components/schemas/StateMinimal'
              - $ref: '#/components/schemas/StateFull'
        total: { type: integer }
        _notice:
          type: string
          nullable: true
          description: Present when results were truncated to 500
        statistics:
          type: object
          properties:
            total_states: { type: integer, example: 5000 }
            countries_represented: { type: integer, example: 250 }
        cache_duration: { type: string, example: immutable }
        available_fields:
          type: array
          items: { type: string }

    StateShowResponse:
      allOf:
        - $ref: '#/components/schemas/StateFull'
        - type: object
          properties:
            country:
              type: object
              description: Overrides the ISO2 string with a country object on show responses
              properties:
                name: { type: string, example: "United States" }
                iso2: { type: string, example: "US" }
            cities_count: { type: integer, example: 1200 }
            _notice:
              type: string
              nullable: true
              description: Present when multiple countries share this state code
            cache_duration: { type: string, example: immutable }

    # ── Currencies ───────────────────────────────────────────
    Currency:
      type: object
      required: [code, name, symbol]
      properties:
        code: { type: string, example: "EUR" }
        name: { type: string, example: "Euro" }
        symbol: { type: string, example: "€" }
        country_count: { type: integer, example: 35 }

    CurrenciesIndexResponse:
      type: object
      properties:
        currencies:
          type: array
          items:
            $ref: '#/components/schemas/Currency'
        total: { type: integer, example: 155 }
        statistics:
          type: object
          properties:
            total_currencies: { type: integer, example: 155 }
            filtered: { type: integer, example: 155 }
        available_fields:
          type: array
          items: { type: string }
          example: ["code", "name", "symbol", "country_count"]
        note: { type: string, example: "Currency definitions only — no live exchange rates" }
        cache_duration: { type: string, example: immutable }

    CurrencyShowResponse:
      allOf:
        - $ref: '#/components/schemas/Currency'
        - type: object
          properties:
            countries:
              type: array
              items:
                type: object
                properties:
                  name: { type: string, example: "Germany" }
                  iso2: { type: string, example: "DE" }
            cache_duration: { type: string, example: immutable }

    # ── Airports ──────────────────────────────────────────────
    AirportMinimal:
      type: object
      properties:
        name: { type: string, example: "John F Kennedy International Airport" }
        iata_code: { type: string, example: "JFK" }
        country:
          type: object
          properties:
            iso2: { type: string, example: "US" }
        airport_type: { type: string, example: "large_airport" }

    AirportFull:
      type: object
      description: |
        On single-airport endpoints (`/airports/{id}`, `/airports/iata/{code}`, `/airports/icao/{code}`)
        `country` is `{name, iso2}` and `state` is an object. On list endpoints with `?all=true`
        (`/airports`, `/airports/country/{iso2}`, `/airports/near`) `country` contains only `iso2`
        and `state` is the subdivision code string (e.g. `"NY"`).
      properties:
        name: { type: string, example: "John F Kennedy International Airport" }
        iata_code: { type: string, example: "JFK" }
        icao_code: { type: string, example: "KJFK" }
        gps_code: { type: string, example: "KJFK" }
        local_code: { type: string, example: "JFK" }
        municipality: { type: string, example: "New York" }
        continent: { type: string, example: "NA" }
        elevation_ft: { type: integer, example: 13 }
        location_label: { type: string, example: "New York, US (JFK)", description: "Pre-built label for dropdowns and autocomplete" }
        country:
          type: object
          properties:
            name: { type: string, example: "United States" }
            iso2: { type: string, example: "US" }
        state:
          nullable: true
          description: Object on single-airport endpoints; subdivision code string on list endpoints
          oneOf:
            - type: object
              properties:
                name: { type: string }
                code: { type: string }
            - type: string
        latitude: { type: number, example: 40.6398 }
        longitude: { type: number, example: -73.7789 }
        airport_type: { type: string, example: "large_airport" }

    AirportWithDistance:
      allOf:
        - $ref: '#/components/schemas/AirportFull'
        - type: object
          properties:
            distance_km: { type: number, example: 12.34, description: "Distance from the requested coordinates" }

    AirportsIndexResponse:
      type: object
      properties:
        airports:
          type: array
          items:
            oneOf:
              - $ref: '#/components/schemas/AirportMinimal'
              - $ref: '#/components/schemas/AirportFull'
        total: { type: integer }
        filters_applied:
          type: array
          items: { type: string }
          description: "Filters active on this response — always includes 'mappable'"
          example: ["mappable", "country"]
        pagination:
          $ref: '#/components/schemas/Pagination'
        statistics:
          type: object
          properties:
            total_airports: { type: integer, example: 67812 }
            with_iata: { type: integer, example: 9319 }
            countries_represented: { type: integer, example: 247 }
        available_types:
          type: array
          items: { type: string }
          example: ["large_airport", "medium_airport", "small_airport", "heliport"]
        available_continents:
          type: array
          items: { type: string }
          example: ["AF", "AN", "AS", "EU", "NA", "OC", "SA"]
        cache_duration: { type: string, example: immutable }
        available_fields:
          type: array
          items: { type: string }

    AirportCountryResponse:
      type: object
      properties:
        country:
          type: object
          properties:
            name: { type: string, example: "Germany" }
            iso2: { type: string, example: "DE" }
        airports:
          type: array
          items:
            oneOf:
              - $ref: '#/components/schemas/AirportMinimal'
              - $ref: '#/components/schemas/AirportFull'
        total: { type: integer, example: 315 }
        type_breakdown:
          type: object
          description: Count of airports by type, sorted by count descending
          example: { "small_airport": 200, "medium_airport": 80, "heliport": 35 }
        cache_duration: { type: string, example: immutable }

    AirportNearResponse:
      type: object
      properties:
        center:
          type: object
          properties:
            lat: { type: number, example: 51.5 }
            lon: { type: number, example: -0.12 }
        radius_km: { type: number, example: 50 }
        airports:
          type: array
          items:
            $ref: '#/components/schemas/AirportWithDistance'
        total: { type: integer, example: 8 }
        cache_duration: { type: string, example: none }

# ─────────────────────────────────────────────────────────────────────────────
# PATHS
# ─────────────────────────────────────────────────────────────────────────────
paths:

  # ── Status ───────────────────────────────────────────────────────────────────
  /status:
    get:
      operationId: getStatus
      summary: Service health and API discovery
      description: |
        Returns service health, all available API endpoints, version information,
        and data coverage statistics. Cache-Control header is set to 1 year immutable.
      tags: [Status]
      responses:
        "200":
          description: Service is operational
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusResponse'
              example:
                service: MatrixAPI
                version: v1
                status: operational
                cache_strategy: immutable

  # ── Vehicles ─────────────────────────────────────────────────────────────────
  /vehicles:
    get:
      operationId: listVehicles
      summary: List all vehicle makes
      description: |
        Returns all vehicle makes. Default response is minimal (name + endpoint) for
        dropdown use. Use `?all=true` for comprehensive data including country and types.

        Filter by `country` (ISO2 or full NHTSA name), `type` (comma-separated),
        `search` (partial name match), or `year` (makes active in that year).
      tags: [Vehicles]
      security:
        - ApiKey: []
      parameters:
        - name: country
          in: query
          description: Filter by manufacturer country — ISO2 (`DE`, `US`) or full NHTSA name (`GERMANY`, `UNITED STATES (USA)`)
          schema: { type: string, example: "DE" }
        - name: type
          in: query
          description: "Filter by vehicle type. Comma-separated: `car`, `mpv`, `suv`, `truck`, `motorcycle`, `moto`, `all`"
          schema: { type: string, example: "car,mpv" }
        - name: search
          in: query
          description: Partial make name search, case-insensitive (e.g. `merc` matches Mercedes-Benz)
          schema: { type: string, example: "merc" }
        - name: year
          in: query
          description: Return only makes that produced at least one model in this calendar year
          schema: { type: integer, example: 1985 }
        - name: all
          in: query
          description: Set to `true` for comprehensive fields (full_name, country, vehicle_types, total_models)
          schema: { type: boolean, example: false }
      responses:
        "200":
          description: List of vehicle makes
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VehicleIndexResponse'
        "429":
          $ref: '#/components/responses/RateLimited'
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'

  /vehicles/by_year/{year}:
    get:
      operationId: listVehiclesByYear
      summary: All makes that produced vehicles in a given year
      description: Returns every vehicle make that had at least one model in the specified year. Useful for historical browsing.
      tags: [Vehicles]
      security:
        - ApiKey: []
      parameters:
        - name: year
          in: path
          required: true
          schema: { type: integer, example: 1985 }
        - name: all
          in: query
          description: Set to `true` for comprehensive make fields
          schema: { type: boolean, example: false }
      responses:
        "200":
          description: Makes active in the specified year
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VehiclesByYearResponse'
        "422":
          description: Year out of range
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'
        "429":
          $ref: '#/components/responses/RateLimited'
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'

  /vehicles/{make}/years:
    get:
      operationId: getVehicleMakeYears
      summary: Production years for a make
      description: Returns the full list of calendar years in which the make produced at least one model. Useful for building year-picker dropdowns.
      tags: [Vehicles]
      security:
        - ApiKey: []
      parameters:
        - name: make
          in: path
          required: true
          schema: { type: string, example: bmw }
      responses:
        "200":
          description: List of production years
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VehicleMakeYearsResponse'
        "404":
          description: Make not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'
        "429":
          $ref: '#/components/responses/RateLimited'
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'

  /vehicles/{make}/models:
    get:
      operationId: getVehicleMakeModels
      summary: All models for a make (year-agnostic)
      description: |
        Returns every distinct model for a make regardless of year — ideal for model dropdowns before a year is selected.
        Each model includes its full list of production years.
      tags: [Vehicles]
      security:
        - ApiKey: []
      parameters:
        - name: make
          in: path
          required: true
          schema: { type: string, example: bmw }
        - name: type
          in: query
          description: "Filter by vehicle type: `car`, `mpv`, `suv`, `truck`, `motorcycle`, `all`"
          schema: { type: string, example: "car" }
        - name: all
          in: query
          description: Set to `true` for comprehensive model fields
          schema: { type: boolean, example: false }
      responses:
        "200":
          description: All models for the make
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VehicleMakeModelsResponse'
        "404":
          description: Make not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'
        "429":
          $ref: '#/components/responses/RateLimited'
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'

  /vehicles/{make}:
    get:
      operationId: getVehicleByMake
      summary: Current-year models for a make
      description: |
        Returns all models for the given make in the most recent model year.
        `make` is case-insensitive (e.g., `bmw`, `BMW`, `Bmw` all work).
      tags: [Vehicles]
      security:
        - ApiKey: []
      parameters:
        - name: make
          in: path
          required: true
          description: Vehicle make name (case-insensitive)
          schema: { type: string, example: bmw }
        - name: type
          in: query
          description: "Filter by vehicle type — comma-separated: `car`, `mpv`, `suv`, `truck`, `motorcycle`, `moto`, `all`. Default: passenger vehicles (car, mpv, truck)"
          schema: { type: string, example: "car" }
        - name: all
          in: query
          description: Set to `true` for comprehensive model data
          schema: { type: boolean }
      responses:
        "200":
          description: Models for the make in the current year
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VehicleShowResponse'
        "404":
          description: Make not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'
        "429":
          $ref: '#/components/responses/RateLimited'
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'

  /vehicles/{year}/{make}:
    get:
      operationId: getVehicleByYearAndMake
      summary: Models for a specific make and year
      description: |
        Returns models for the given make in the specified model year (1981–2026).
        `make` is case-insensitive.
      tags: [Vehicles]
      security:
        - ApiKey: []
      parameters:
        - name: year
          in: path
          required: true
          description: 4-digit model year (1981–2026)
          schema: { type: integer, example: 2025 }
        - name: make
          in: path
          required: true
          description: Vehicle make name (case-insensitive)
          schema: { type: string, example: bmw }
        - name: type
          in: query
          description: "Filter by vehicle type — comma-separated: `car`, `mpv`, `suv`, `truck`, `motorcycle`, `moto`, `all`. Default: passenger vehicles (car, mpv, truck)"
          schema: { type: string, example: "car" }
        - name: all
          in: query
          description: Set to `true` for comprehensive model data
          schema: { type: boolean }
      responses:
        "200":
          description: Models for the make in the given year
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VehicleShowResponse'
        "404":
          description: Make or year not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'
        "429":
          $ref: '#/components/responses/RateLimited'
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'

  /vehicles/{make}/{model}:
    get:
      operationId: getVehicleByMakeAndModel
      summary: Current-year details for a specific make and model
      description: Returns detailed data for a specific vehicle model in the most recent model year.
      tags: [Vehicles]
      security:
        - ApiKey: []
      parameters:
        - name: make
          in: path
          required: true
          schema: { type: string, example: bmw }
        - name: model
          in: path
          required: true
          schema: { type: string, example: "3-series" }
      responses:
        "200":
          description: Vehicle model details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VehicleShowResponse'
        "404":
          description: Make or model not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'
        "429":
          $ref: '#/components/responses/RateLimited'
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'

  /vehicles/{year}/{make}/{model}:
    get:
      operationId: getVehicleByYearMakeModel
      summary: Details for a specific make, year, and model
      description: Returns detailed data for a specific vehicle model in a given year.
      tags: [Vehicles]
      security:
        - ApiKey: []
      parameters:
        - name: year
          in: path
          required: true
          schema: { type: integer, example: 2025 }
        - name: make
          in: path
          required: true
          schema: { type: string, example: bmw }
        - name: model
          in: path
          required: true
          schema: { type: string, example: "3-series" }
      responses:
        "200":
          description: Vehicle model details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VehicleShowResponse'
        "404":
          description: Make or model not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'
        "429":
          $ref: '#/components/responses/RateLimited'
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'

  # ── Countries ─────────────────────────────────────────────────────────────────
  /countries:
    get:
      operationId: listCountries
      summary: List all countries
      description: |
        Returns all 250 countries. Default response is minimal (name, iso2, iso3, emoji, region, endpoint).
        Use `?all=true` for the full comprehensive response.

        **Filters:** `region`, `subregion`, `search`, `currency`, `driving_side`, `landlocked`, `un_member`, `language`, `religion`, `phone_code`
      tags: [Countries]
      parameters:
        - name: region
          in: query
          description: "Filter by region: `Africa`, `Americas`, `Asia`, `Europe`, `Oceania`, `Polar`"
          schema: { type: string, example: "Europe" }
        - name: subregion
          in: query
          description: "Filter by subregion (e.g., `Western Europe`, `Southern Asia`)"
          schema: { type: string, example: "Western Europe" }
        - name: search
          in: query
          description: "Search by country name (partial match) or exact ISO2/ISO3 code"
          schema: { type: string, example: "ger" }
        - name: currency
          in: query
          description: "Filter by currency code (e.g., `EUR`, `USD`)"
          schema: { type: string, example: "EUR" }
        - name: driving_side
          in: query
          description: Filter by which side of the road traffic drives on
          schema: { type: string, enum: [left, right], example: "right" }
        - name: landlocked
          in: query
          description: Filter to landlocked countries only
          schema: { type: boolean, example: false }
        - name: un_member
          in: query
          description: Filter by UN membership status
          schema: { type: boolean, example: true }
        - name: language
          in: query
          description: Filter by primary language (partial match)
          schema: { type: string, example: "spanish" }
        - name: religion
          in: query
          description: Filter by main religion (partial match)
          schema: { type: string, example: "islam" }
        - name: phone_code
          in: query
          description: "Filter by phone dialing code (e.g., `1`, `44`, `49`)"
          schema: { type: string, example: "49" }
        - name: all
          in: query
          description: Set to `true` for full comprehensive response instead of minimal 6-field response
          schema: { type: boolean, example: false }
      responses:
        "200":
          description: List of countries
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CountryIndexResponse'

  /countries/{code}:
    get:
      operationId: getCountry
      summary: Single country by ISO code or name
      description: |
        Returns full country data. `code` accepts:
        - ISO2 code: `DE`
        - ISO3 code: `DEU`
        - Country name (case-insensitive): `germany`
      tags: [Countries]
      parameters:
        - name: code
          in: path
          required: true
          description: ISO2 code, ISO3 code, or country name (case-insensitive)
          schema: { type: string, example: "DE" }
      responses:
        "200":
          description: Country details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CountryShowResponse'
        "404":
          description: Country not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StructuredError'

  /countries/{code}/subdivisions:
    get:
      operationId: getCountrySubdivisions
      summary: Subdivisions for a country
      description: |
        Returns all administrative subdivisions (states, provinces, departments, etc.)
        for the given country. The subdivision `type` field reflects the country-specific
        terminology (e.g., "states" for US, "departments" for France).
      tags: [Subdivisions]
      parameters:
        - name: code
          in: path
          required: true
          description: ISO2 country code
          schema: { type: string, example: "US" }
      responses:
        "200":
          description: List of subdivisions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubdivisionsResponse'
        "404":
          description: Country not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StructuredError'

  /countries/{code}/cities:
    get:
      operationId: getCountryCities
      summary: All cities in a country
      description: |
        Returns all cities for a country, optionally filtered by a subdivision code.
        Use `?state=CA` to filter to a single subdivision.
      tags: [Cities]
      parameters:
        - name: code
          in: path
          required: true
          description: ISO2 country code
          schema: { type: string, example: "XK" }
        - name: state
          in: query
          description: Filter by subdivision/state code (e.g., `CA`, `BE`)
          schema: { type: string, example: "XUF" }
      responses:
        "200":
          description: Cities for the country
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CountryCitiesResponse'
        "404":
          description: Country not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StructuredError'

  /countries/{code}/subdivisions/{subdivision_code}/cities:
    get:
      operationId: getSubdivisionCities
      summary: Cities in a subdivision (canonical URL)
      description: |
        Returns all cities within the given subdivision. This is the **canonical URL** form.
        Human-readable aliases (`/countries/{name}/{subdivision}/cities`) redirect here via
        the `Link: rel="canonical"` response header.

        Returns **202 Accepted** if city data is still being populated by a background job.
        Poll again after `retry_after` seconds.
      tags: [Cities]
      parameters:
        - name: code
          in: path
          required: true
          description: ISO2 country code
          schema: { type: string, example: "XK" }
        - name: subdivision_code
          in: path
          required: true
          description: Subdivision code (e.g., `XUF`, `CA`, `BY`) — also accepts the subdivision name (case-insensitive)
          schema: { type: string, example: "XUF" }
      responses:
        "200":
          description: Cities in the subdivision
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CitiesResponse'
        "202":
          description: City data is being populated asynchronously
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AsyncPending'
        "404":
          description: Country or subdivision not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StructuredError'

  /countries/{country_identifier}/{subdivision_identifier}:
    get:
      operationId: getSubdivisionDetails
      summary: Subdivision details (human-readable URL)
      description: |
        Returns details for a subdivision using human-readable, case-insensitive names.
        Both `{country_identifier}` and `{subdivision_identifier}` accept ISO codes or
        full names (e.g., `/countries/germany/bavaria` or `/countries/DE/Bavaria`).

        The response includes a `Link: rel="canonical"` header pointing to the ISO-code URL.
      tags: [Subdivisions]
      parameters:
        - name: country_identifier
          in: path
          required: true
          description: ISO2 code or country name (case-insensitive)
          schema: { type: string, example: "kosovo" }
        - name: subdivision_identifier
          in: path
          required: true
          description: Subdivision code or subdivision name (case-insensitive)
          schema: { type: string, example: "ferizaj" }
      responses:
        "200":
          description: Subdivision details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubdivisionDetailsResponse'
        "404":
          description: Country or subdivision not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StructuredError'

  /countries/{country_identifier}/{subdivision_identifier}/cities:
    get:
      operationId: getSubdivisionCitiesHumanReadable
      summary: Cities in a subdivision (human-readable URL)
      description: |
        Human-readable alias for `/countries/{code}/subdivisions/{subdivision_code}/cities`.
        Both identifiers accept ISO codes or full names (case-insensitive).

        Response includes a `Link: rel="canonical"` header pointing to the ISO-code canonical URL.

        Returns **202 Accepted** if city data is still being populated by a background job.
      tags: [Cities]
      parameters:
        - name: country_identifier
          in: path
          required: true
          description: ISO2 code or country name (case-insensitive)
          schema: { type: string, example: "kosovo" }
        - name: subdivision_identifier
          in: path
          required: true
          description: Subdivision code or subdivision name (case-insensitive)
          schema: { type: string, example: "ferizaj" }
      responses:
        "200":
          description: Cities in the subdivision
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CitiesHumanReadableResponse'
        "202":
          description: City data is being populated asynchronously
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AsyncPending'
        "404":
          description: Country or subdivision not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StructuredError'

  /countries/{country_identifier}/{subdivision_identifier}/{city_identifier}:
    get:
      operationId: getCityDetails
      summary: Single city details (human-readable URL)
      description: |
        Returns details for a specific city. All three identifiers accept ISO codes or
        full names (case-insensitive).

        Response includes a `Link: rel="canonical"` header pointing to the ISO-code canonical URL.

        Returns **202 Accepted** if the parent subdivision's city data is still being populated.
      tags: [Cities]
      parameters:
        - name: country_identifier
          in: path
          required: true
          description: ISO2 code or country name (case-insensitive)
          schema: { type: string, example: "kosovo" }
        - name: subdivision_identifier
          in: path
          required: true
          description: Subdivision code or subdivision name (case-insensitive)
          schema: { type: string, example: "ferizaj" }
        - name: city_identifier
          in: path
          required: true
          description: City name (case-insensitive)
          schema: { type: string, example: "shtime" }
      responses:
        "200":
          description: City details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CityDetailsResponse'
        "202":
          description: Parent subdivision city data is still being populated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AsyncPending'
        "404":
          description: Country, subdivision, or city not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StructuredError'

  # ── Regions ──────────────────────────────────────────────────────────────────
  /regions:
    get:
      operationId: listRegions
      summary: List all world regions
      description: Returns the 6 world regions (Africa, Americas, Asia, Europe, Oceania, Polar) with country counts.
      tags: [Regions]
      responses:
        "200":
          description: List of regions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegionsIndexResponse'

  /regions/{id}:
    get:
      operationId: getRegion
      summary: Single region with subregions and countries
      description: Returns a region by numeric ID or name with its subregions and countries.
      tags: [Regions]
      parameters:
        - name: id
          in: path
          required: true
          description: Region numeric ID or name (e.g., `Europe`)
          schema: { type: string, example: "Europe" }
      responses:
        "200":
          description: Region details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RegionShowResponse'
        "404":
          description: Region not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StructuredError'

  # ── Subregions ────────────────────────────────────────────────────────────────
  /subregions:
    get:
      operationId: listSubregions
      summary: List all subregions
      description: Returns all 22 geographic subregions. Filter by parent region using `?region=`.
      tags: [Subregions]
      parameters:
        - name: region
          in: query
          description: "Filter by parent region name (e.g., `Europe`)"
          schema: { type: string, example: "Europe" }
      responses:
        "200":
          description: List of subregions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubregionsIndexResponse'

  /subregions/{id}:
    get:
      operationId: getSubregion
      summary: Single subregion
      description: Returns a subregion by numeric ID or name.
      tags: [Subregions]
      parameters:
        - name: id
          in: path
          required: true
          description: Subregion numeric ID or name (e.g., `Western Europe`)
          schema: { type: string, example: "Western Europe" }
      responses:
        "200":
          description: Subregion details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubregionShowResponse'
        "404":
          description: Subregion not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StructuredError'

  # ── States (standalone) ────────────────────────────────────────────────────────
  /states:
    get:
      operationId: listStates
      summary: List subdivisions across all countries
      description: |
        Standalone subdivisions endpoint useful for cross-country queries.
        Results are truncated to 500 records — **always filter by country** for a complete set.

        Alias: `/subdivisions` returns identical data.
      tags: [States]
      parameters:
        - name: country
          in: query
          description: ISO2 country code filter — **strongly recommended**
          schema: { type: string, example: "US" }
        - name: search
          in: query
          description: Search by subdivision name (partial match) or exact subdivision code
          schema: { type: string, example: "california" }
        - name: all
          in: query
          description: Set to `true` for comprehensive fields (type, latitude, longitude)
          schema: { type: boolean }
      responses:
        "200":
          description: List of states/subdivisions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatesIndexResponse'

  /states/{id}:
    get:
      operationId: getState
      summary: Single subdivision by ID or code
      description: |
        Returns a subdivision by its numeric database ID or state code (e.g., `CA`).
        State codes are **not unique across countries** — pass `?country={iso2}` to
        disambiguate, or use the numeric ID.
      tags: [States]
      parameters:
        - name: id
          in: path
          required: true
          description: Numeric ID or state code (e.g., `CA`)
          schema: { type: string, example: "CA" }
        - name: country
          in: query
          description: ISO2 country code to disambiguate state codes shared across countries
          schema: { type: string, example: "US" }
      responses:
        "200":
          description: Subdivision details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StateShowResponse'
        "404":
          description: Subdivision not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StructuredError'

  /subdivisions:
    get:
      operationId: listSubdivisions
      summary: List subdivisions across all countries (alias for /states)
      description: |
        Alias for `GET /states`. Returns identical data and supports the same parameters.
        Use whichever URL is more natural for your use case.
      tags: [States]
      parameters:
        - name: country
          in: query
          description: ISO2 country code filter — **strongly recommended**
          schema: { type: string, example: "DE" }
        - name: search
          in: query
          description: Search by subdivision name (partial match) or exact subdivision code
          schema: { type: string, example: "bavaria" }
        - name: all
          in: query
          description: Set to `true` for comprehensive fields
          schema: { type: boolean }
      responses:
        "200":
          description: List of subdivisions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatesIndexResponse'

  /subdivisions/{id}:
    get:
      operationId: getSubdivision
      summary: Single subdivision by ID or code (alias for /states/{id})
      description: Alias for `GET /states/{id}`. Returns identical data and supports the same `?country=` disambiguation parameter.
      tags: [States]
      parameters:
        - name: id
          in: path
          required: true
          description: Numeric ID or state code (e.g., `BY`)
          schema: { type: string, example: "BY" }
        - name: country
          in: query
          description: ISO2 country code to disambiguate state codes shared across countries
          schema: { type: string, example: "DE" }
      responses:
        "200":
          description: Subdivision details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StateShowResponse'
        "404":
          description: Subdivision not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StructuredError'

  # ── Currencies ────────────────────────────────────────────────────────────────
  /currencies:
    get:
      operationId: listCurrencies
      summary: List all currencies
      description: Returns ISO 4217 currency reference data derived from country records. Static definitions only — no live exchange rates.
      tags: [Currencies]
      parameters:
        - name: search
          in: query
          description: Search by currency code or name (partial match, case-insensitive)
          schema: { type: string, example: "dollar" }
      responses:
        "200":
          description: List of currencies
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CurrenciesIndexResponse'

  /currencies/{id}:
    get:
      operationId: getCurrency
      summary: Single currency by ISO 4217 code
      description: Returns currency details for the given ISO 4217 code (e.g., `EUR`, `USD`, `GBP`).
      tags: [Currencies]
      parameters:
        - name: id
          in: path
          required: true
          description: ISO 4217 currency code (case-insensitive)
          schema: { type: string, example: "EUR" }
      responses:
        "200":
          description: Currency details including the countries using it
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CurrencyShowResponse'
        "404":
          description: Currency not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StructuredError'

  # ── Cities (standalone) ────────────────────────────────────────────────────────
  /cities:
    get:
      operationId: listCities
      summary: List cities with cross-country/subdivision filtering
      description: |
        Returns cities filtered by country and/or state. Results are truncated to 1000 —
        always provide at least a `country` filter for a complete set.

        Default response is minimal (id, name, state_code, country_iso2).
        Use `?all=true` for coordinates and full names.
      tags: [Cities]
      parameters:
        - name: country
          in: query
          description: Filter by ISO2 country code
          schema: { type: string, example: "US" }
        - name: state
          in: query
          description: Filter by subdivision code
          schema: { type: string, example: "CA" }
        - name: search
          in: query
          description: Search by city name (partial match, case-insensitive)
          schema: { type: string, example: "paris" }
        - name: all
          in: query
          description: Set to `true` for comprehensive fields including coordinates
          schema: { type: boolean }
      responses:
        "200":
          description: List of cities
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CityIndexResponse'

  /cities/{id}:
    get:
      operationId: getCity
      summary: Single city by database ID
      description: Returns details for a city by its numeric database ID.
      tags: [Cities]
      parameters:
        - name: id
          in: path
          required: true
          description: City numeric database ID
          schema: { type: integer, example: 157206 }
      responses:
        "200":
          description: City details with nested state, country, and coordinates
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CityShowResponse'
        "404":
          description: City not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StructuredError'

  # ── Airports ──────────────────────────────────────────────────────────────────
  /airports:
    get:
      operationId: listAirports
      summary: List airports with filtering and pagination
      description: |
        Returns airports paginated at 100 per page. Filter by country, continent, region,
        type, or look up by IATA code.

        Default response includes name, iata_code, country, and airport_type.
        Use `?all=true` for full fields including coordinates, ICAO, elevation, and state.
      tags: [Airports]
      security:
        - ApiKey: []
      parameters:
        - name: country
          in: query
          description: Filter by ISO2 country code
          schema: { type: string, example: "US" }
        - name: continent
          in: query
          description: "Filter by continent code: `AF`, `AN`, `AS`, `EU`, `NA`, `OC`, `SA`"
          schema: { type: string, example: "EU" }
        - name: region
          in: query
          description: "Filter by region name (e.g., `Europe`)"
          schema: { type: string, example: "Europe" }
        - name: subregion
          in: query
          description: "Filter by subregion name (e.g., `Western Europe`)"
          schema: { type: string, example: "Western Europe" }
        - name: type
          in: query
          description: "Filter by airport type — comma-separated supported: large_airport, medium_airport, small_airport, heliport, seaplane_base, balloonport, closed"
          schema: { type: string, example: "large_airport,medium_airport" }
        - name: search
          in: query
          description: Partial name or municipality search, case-insensitive (e.g. `heathrow`, `Kennedy`)
          schema: { type: string, example: "heathrow" }
        - name: exclude_closed
          in: query
          description: "Set to true to omit decommissioned airports (airport_type: closed)"
          schema: { type: boolean, example: true }
        - name: iata
          in: query
          description: "Deprecated — look up a single airport by IATA code. Use GET /airports/iata/{code} instead."
          schema: { type: string, example: "JFK" }
          deprecated: true
        - name: all
          in: query
          description: "Set to true for full fields including coordinates, ICAO, elevation, location_label"
          schema: { type: boolean }
        - name: per_page
          in: query
          description: "Results per page (default: 100, max: 500)"
          schema: { type: integer, default: 100, example: 200 }
        - name: page
          in: query
          description: "Page number (default: 1)"
          schema: { type: integer, default: 1, example: 1 }
      responses:
        "200":
          description: Paginated list of airports
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AirportsIndexResponse'
        "404":
          description: No airport matches the deprecated `?iata=` lookup
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'
        "429":
          $ref: '#/components/responses/RateLimited'
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'

  /airports/iata/{code}:
    get:
      operationId: getAirportByIata
      summary: Single airport by IATA code
      description: |
        Returns full airport details for a given IATA code (e.g. `JFK`, `LHR`, `CDG`).
        Preferred over `GET /airports/{id}` when you have a known IATA code.
      tags: [Airports]
      security:
        - ApiKey: []
      parameters:
        - name: code
          in: path
          required: true
          schema: { type: string, example: "JFK" }
      responses:
        "200":
          description: Airport details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AirportFull'
        "404":
          description: IATA code not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'
        "429":
          $ref: '#/components/responses/RateLimited'
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'

  /airports/icao/{code}:
    get:
      operationId: getAirportByIcao
      summary: Single airport by ICAO code
      description: Returns full airport details for a given 4-character ICAO code (e.g. `KJFK`, `EGLL`, `LFPG`).
      tags: [Airports]
      security:
        - ApiKey: []
      parameters:
        - name: code
          in: path
          required: true
          schema: { type: string, example: "KJFK" }
      responses:
        "200":
          description: Airport details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AirportFull'
        "404":
          description: ICAO code not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'
        "429":
          $ref: '#/components/responses/RateLimited'
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'

  /airports/country/{iso2}:
    get:
      operationId: getAirportsByCountry
      summary: All airports in a country (unpaginated)
      description: |
        Returns every mappable airport for the given ISO2 country code as a single flat list — no pagination.
        Most countries have fewer than 300 airports so a single response is practical.

        Optionally filter by `type` or exclude closed airports with `exclude_closed=true`.
      tags: [Airports]
      security:
        - ApiKey: []
      parameters:
        - name: iso2
          in: path
          required: true
          schema: { type: string, example: "DE" }
        - name: type
          in: query
          description: Filter by airport type — comma-separated supported
          schema: { type: string, example: "large_airport,medium_airport" }
        - name: exclude_closed
          in: query
          description: Set to `true` to omit closed airports
          schema: { type: boolean }
        - name: all
          in: query
          description: Set to `true` for full fields
          schema: { type: boolean }
      responses:
        "200":
          description: All airports in the country
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AirportCountryResponse'
        "404":
          description: Country code not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'
        "429":
          $ref: '#/components/responses/RateLimited'
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'

  /airports/near:
    get:
      operationId: getAirportsNear
      summary: Airports within a radius of a coordinate
      description: |
        Returns airports within `radius_km` of the given `lat`/`lon` using the Haversine formula.
        Results are sorted by distance ascending. No PostGIS required — pure SQL math.

        Results include a `distance_km` field on each airport.
        Response is **not cached** (`cache_duration: "none"`) since coordinate combinations are unbounded.
      tags: [Airports]
      security:
        - ApiKey: []
      parameters:
        - name: lat
          in: query
          required: true
          description: Latitude of the search centre
          schema: { type: number, example: 51.5 }
        - name: lon
          in: query
          required: true
          description: Longitude of the search centre
          schema: { type: number, example: -0.12 }
        - name: radius_km
          in: query
          description: "Search radius in kilometres (default: 50, min: 1, max: 500)"
          schema: { type: number, default: 50, example: 100 }
        - name: type
          in: query
          description: Filter by airport type — comma-separated supported
          schema: { type: string, example: "large_airport" }
        - name: all
          in: query
          description: Set to `true` for full fields
          schema: { type: boolean }
      responses:
        "200":
          description: Airports near the given coordinates
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AirportNearResponse'
        "422":
          description: Missing lat or lon parameter
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'
        "429":
          $ref: '#/components/responses/RateLimited'
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'

  /airports/{id}:
    get:
      operationId: getAirport
      summary: Single airport by IATA, ICAO, GPS code, or database ID
      description: |
        Returns full airport details. `{id}` accepts:
        - IATA code: `JFK`
        - ICAO code: `KJFK`
        - GPS code: `KJFK`
        - Numeric database ID

        **Prefer `/airports/iata/{code}` or `/airports/icao/{code}`** for intent-specific lookups.
      tags: [Airports]
      security:
        - ApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          description: IATA code, ICAO code, GPS code, or numeric database ID
          schema: { type: string, example: "JFK" }
      responses:
        "200":
          description: Airport details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AirportFull'
        "404":
          description: Airport not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'
        "429":
          $ref: '#/components/responses/RateLimited'
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'

  /events:
    get:
      operationId: listEvents
      summary: List events
      description: |
        Returns upcoming events from US local news sources.
        Supports geo-proximity search, date range, category, and keyword filtering.
      tags: [Events]
      security:
        - ApiKey: []
      parameters:
        - name: lat
          in: query
          description: Latitude for geo proximity search (requires `lng`)
          schema: { type: number, format: float, example: 39.4143 }
        - name: lng
          in: query
          description: Longitude for geo proximity search (requires `lat`)
          schema: { type: number, format: float, example: -77.4105 }
        - name: radius
          in: query
          description: Search radius in km (default 50)
          schema: { type: number, format: float, example: 30 }
        - name: from
          in: query
          description: Start date filter (ISO8601)
          schema: { type: string, format: date, example: "2026-06-01" }
        - name: to
          in: query
          description: End date filter (ISO8601)
          schema: { type: string, format: date, example: "2026-06-30" }
        - name: category
          in: query
          description: Filter by event category
          schema: { type: string, example: "music" }
        - name: state
          in: query
          description: Filter by US state code
          schema: { type: string, example: "MD" }
        - name: city
          in: query
          description: Filter by city name
          schema: { type: string, example: "Frederick" }
        - name: q
          in: query
          description: Full-text search in title and description
          schema: { type: string, example: "farmers market" }
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: per_page
          in: query
          schema: { type: integer, default: 100, maximum: 500 }
      responses:
        "200":
          description: List of events
          content:
            application/json:
              schema:
                type: object
                properties:
                  events:
                    type: array
                    items:
                      $ref: '#/components/schemas/EventSummary'
                  total: { type: integer, example: 42 }
                  pagination:
                    $ref: '#/components/schemas/Pagination'
                  statistics:
                    type: object
                    properties:
                      total_events: { type: integer, example: 42 }
                      upcoming_events: { type: integer, example: 38 }
                  cache_duration: { type: string, example: "15 minutes" }
                  examples:
                    type: object
                    properties:
                      near_me: { type: string, example: "https://matrixapi.dev/api/v1/events?lat=39.41&lng=-77.41&radius=30" }
                      this_weekend: { type: string, example: "https://matrixapi.dev/api/v1/events?from=2026-05-08&to=2026-05-10" }
                      by_category: { type: string, example: "https://matrixapi.dev/api/v1/events?category=music" }
                      search: { type: string, example: "https://matrixapi.dev/api/v1/events?q=farmers+market" }
                  query_parameters:
                    type: object
                    additionalProperties: { type: string }
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'

  /events/{id}:
    get:
      operationId: getEvent
      summary: Get event by ID
      description: Returns a single event with full detail including venue coordinates, description, pricing, and images.
      tags: [Events]
      security:
        - ApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer, example: 4821 }
      responses:
        "200":
          description: Event detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  event:
                    $ref: '#/components/schemas/EventFull'
        "404":
          description: Event not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FlatError'

