openapi: 3.1.0
info:
  title: cca-aerospace API
  version: "1.0.0"
  summary: FAA / aerospace open-data mirror, archive & intelligence API.
  description: |
    **cca-aerospace** is a permanent mirror, archive and intelligence API over public
    FAA / aerospace open data, part of the Cooperative Computing Alliance (CCA)
    data-service fleet.

    Every JSON response uses the fleet **envelope**
    `{ request_status, message, [error_code], data, meta }`. The `meta` block always
    reports `sources[]`, `cache`, `upstream`, the AIRAC/date `cycle` (for cycle-versioned
    data) and `as_of`.

    ## Access posture (load-bearing)
    This API is **private**. HTTP access to `aerospace.datasourceapi.com` is gated by a
    zone WAF rule (an `X-Auth` header) and the primary callers are other fleet workers via
    Cloudflare **service bindings**. Aircraft (N-number registry) and airman
    (certification) records carry **PII** and are stored and served at **full fidelity,
    forever, with no redaction** — this is permissible *only* because the read surface
    stays private/WAF-gated and the only mutation surface stays behind `X-Admin-Key`. The
    private posture is the privacy control; these endpoints must never be exposed on a
    public, unauthenticated route.

    ## Mirror purity
    All reads are served from the local mirror (D1 hot index + R2 system-of-record). No
    upstream is ever called in the request path; an entity whose cycle has not been
    ingested yet returns `ERROR_NOT_IN_MIRROR`, never a live fetch.

    ## Cross-cutting surface
    Every collection (`airport`, `aircraft`, `airman`, `charts`) inherits the same
    history / changes / raw / snapshots / compare sub-resources and `?as_of=` time-travel,
    plus result + time-sequence **permalinks** that freeze a response forever.

    ## License watch
    The FAA airmen file is monthly, so watched airmen are additionally re-checked
    **daily** against the live FAA Airmen Inquiry (cron 08:17 UTC; FAA sessions are
    paced 20 seconds apart, and a failed session gets one retry after a 60-second
    backoff. A persistent upstream failure circuit-breaks the batch instead of
    hammering FAA; failed watches become eligible for the half-hourly recovery cron
    after one hour. A new certificate or rating surfaces as a field-level diff in
    `/airman/{id}/changes` and the global `/changes` feed within a day; an unchanged
    rescan writes nothing (re-persists are deduped per cycle). Watches enroll
    automatically whenever an airman is captured by a live search or linked to a CCA
    identity, and can be managed via `/admin/airman/watch*`. Each failed attempt emits
    a structured Worker log and records bounded diagnostic text on the watch row.

    ## Fleet service bindings & CCA identity mapping
    Same-account CCA workers call this API over the `AEROSPACE` Cloudflare **service
    binding** with **no keys at all** — binding traffic never traverses the zone edge,
    which is how the worker recognizes it. Reads need no `X-Auth`; the CCA-identity
    mapping surface (`GET/POST /admin/mci`, `POST /admin/mci/unlink`) also trusts
    binding callers keylessly, so fleet services can link FAA airmen (by UNIQUE ID) and
    aircraft operators (by registration mark) to **Matrix Consumer Identifiers**
    programmatically with zero shared secrets. Links live in a read-time CCA-local
    layer (`mci_links`) — the mirror stays pure upstream — and resolve via
    `GET /mci/{mci}`, `GET /airman/search?mci=` and the `data.cca` block on linked
    records. Edge (HTTP) callers still need `X-Auth` + `X-Admin-Key`; mirror-data
    mutation (ingest / backfill / overrides) stays key-gated even over the binding.
  contact:
    name: Cooperative Computing Alliance
    url: https://aerospace.datasourceapi.com/api/v1/docs
  license:
    name: Public domain source data (FAA); see attribution per dataset.
    url: https://www.faa.gov/

servers:
  - url: https://aerospace.datasourceapi.com
    description: Production (WAF-gated; X-Auth required for HTTP callers)

security:
  - wafAuth: []

tags:
  - name: Ops
    description: Service + storage health.
  - name: Map
    description: Vector overlays + search for the map-first viewer.
  - name: Airports
    description: NASR airports & runways (the canonical aeronautical entity).
  - name: Navigation
    description: NASR navaids, fixes & airways (the nav-data foundation for routing).
  - name: Aircraft
    description: FAA N-number registry. Carries PII — private/WAF-gated only.
  - name: Airmen
    description: FAA airmen certification. Carries PII — private/WAF-gated only.
  - name: Charts
    description: Georeferenced chart product catalog (tiles served by cca-faacharts).
  - name: Procedures
    description: |
      FAA **CIFP** (Coded Instrument Flight Procedures / ARINC 424) — the
      computer-readable instrument procedures (SIDs / STARs / approaches) that EFB apps
      like ForeFlight draw. The "charts as data" companion to the d-TPP plate PDFs and
      the raster chart tiles. 28-day AIRAC; mirror-only; no PII.
  - name: Obstacles
    description: FAA Digital Obstacle File (DOF) — towers/antennas/buildings. Mirror-only; no PII.
  - name: Reference
    description: Mirror-only reference lists — OurAirports ISO 3166 countries / regions.
  - name: Derived
    description: |
      Read-time `aerospace-derived` intelligence computed over the existing mirror —
      nearest-feature, great-circle routing and corridor/TFR analysis. No upstream is
      ever called and nothing is persisted: `meta.sources` includes `aerospace-derived`
      and `meta.upstream.status` is `skipped`.
  - name: Realtime
    description: |
      Continuous-capture ephemeral feeds — weather (METAR/TAF/SIGMET) and TFRs.
      These are NOT cycle-versioned; each poll is captured with effective/expire
      timestamps into a dedicated ephemeral tier (KV latest + a separate R2 bucket)
      and pruned at 90 days. NOTAM is credential-gated (501) until provisioned.
  - name: History & change
    description: Per-entity history / changes / raw / snapshots / compare + as-of.
  - name: Global
    description: Cross-entity change feed and general compare.
  - name: Permalinks
    description: Freeze a result or time-sequence and replay it verbatim forever.
  - name: Admin
    description: Gated ingest (X-Admin-Key). The only mutation surface.

paths:
  /api/v1/status:
    get:
      tags: [Ops]
      summary: Service, storage & upstream health
      description: Per-dataset freshness, storage counts and binding health. `?probe=true` adds live binding probes.
      parameters:
        - name: probe
          in: query
          schema: { type: boolean }
          description: Run live binding probes (slower).
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "503": { $ref: "#/components/responses/Envelope" }

  /api/v1/sources:
    get:
      tags: [Ops]
      summary: Data-source health & observability census
      description: |
        Read-only, mirror-only census of EVERY upstream product we mirror. A static catalog
        (what we pull, from where, how often) is joined at request time with live mirror state
        from the `cycles` ledger, the `change_log` and the per-domain shards: the most-recent
        pull, that pull's field-level diff (added/removed/modified), history depth, archive
        status, a cheap per-shard record count and a freshness/health badge. Continuous feeds
        (weather/winds/TFR) have no AIRAC cycle, so their `last_pull` is read from the realtime
        KV capture markers instead — surfacing the REAL latest `captured_at`, an age and a
        `stale` flag (health flips to `stale` when a capture has stalled past its cadence);
        without a capture marker they stay `continuous` with `last_pull: null`. Makes NO upstream
        call and persists nothing; a missing binding/table is skipped (degrade, never 500).
        `meta.cache.status` is always `bypass` (this view is computed, never cached). Non-PII
        by construction — only aggregate counts and source metadata are returned, never an
        individual aircraft/airman row. Powers the `/sources.html` viewer.
      responses:
        "200":
          description: The data-source census.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties:
                      data: { $ref: "#/components/schemas/DataSourceCensus" }

  /api/v1/search:
    get:
      tags: [Map]
      summary: Omni search (classifier)
      description: |
        Single front-door search. Fans a free-text query across the non-PII spatial
        collections (airports, navaids, fixes, airways) and, when the query is an exact
        aircraft N-number, airman FAA UNIQUE ID, DOF obstacle OAS number, or ISO 3166-1
        country code, returns a direct resolve link to that entity (the aircraft/airman
        links land on the WAF-gated detail hub). It never enumerates aircraft-owner or
        airman-person PII — a free-text name only discovers airports/navaids/fixes/airways
        here; full owner/person search lives on `/api/v1/aircraft/search` and
        `/api/v1/airman/search`. Powers the viewer's home search box.
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string }
          example: KSFO
        - { $ref: "#/components/parameters/limit" }
      responses:
        "200":
          description: Ranked, uniform search results across families.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties:
                      data:
                        type: array
                        items:
                          type: object
                          properties:
                            kind: { type: string, enum: [airport, navaid, fix, airway, obstacle, aircraft, airman, country] }
                            id: { type: string }
                            label: { type: string }
                            sub: { type: string }
                            href: { type: string }
                            authority: { type: string, nullable: true }
                            country: { type: string, nullable: true }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }

  /api/v1/airports.geojson:
    get:
      tags: [Map]
      summary: Airports vector overlay (raw GeoJSON)
      description: |
        A raw `application/geo+json` **FeatureCollection** of mirrored airports for the map
        overlay. This endpoint is **not** wrapped in the fleet envelope — MapLibre's
        `geojson` source needs a bare FeatureCollection. Provenance is surfaced in the
        `X-Cycle` and `X-Source` response headers. Mirror-only; airports carry no PII.
      parameters:
        - name: bbox
          in: query
          schema: { type: string }
          example: "-74.3,40.4,-73.5,40.9"
          description: |
            Clip to a bounding box `w,s,e,n` (minLon,minLat,maxLon,maxLat). A west
            longitude greater than east crosses the antimeridian, for example
            `170,-25,-170,0` for a South Pacific window.
        - name: type
          in: query
          schema: { type: string }
          example: large_airport
          description: Filter by airport type.
        - name: authority
          in: query
          schema: { type: string }
          description: Source authority filter. Omit to include every mirrored authority.
        - name: country
          in: query
          schema: { type: string, example: US }
        - name: limit
          in: query
          schema: { type: integer, default: 5000, maximum: 20000 }
        - name: significant
          in: query
          schema: { type: boolean }
          description: |
            Keep only significant airfields (large/medium/airport/small), dropping the long
            tail of seaplane bases, gliderports, ultralights, balloonports and heliports.
            The viewer sets this at low (continental) zoom so the result cap isn't eaten by
            minor facilities. Display filter only — rows stay in the mirror and remain
            queryable.
        - name: include_closed
          in: query
          schema: { type: boolean }
          description: |
            Include `closed` airports, which are excluded by default (display noise at
            scale). Closed rows are never removed from the mirror; this only affects what
            the overlay returns. Implied when `type=closed`.
        - name: refresh
          in: query
          schema: { type: boolean }
          description: Bypass the KV hot cache.
      responses:
        "200":
          description: A GeoJSON FeatureCollection of airports.
          headers:
            X-Cycle: { schema: { type: string }, description: Newest cycle represented. }
            X-Source: { schema: { type: string }, description: Provenance (e.g. faa-nasr). }
            X-Cache: { schema: { type: string, enum: [hit, miss, bypass] } }
          content:
            application/geo+json:
              schema: { $ref: "#/components/schemas/AirportFeatureCollection" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }

  /api/v1/airport/search:
    get:
      tags: [Map, Airports]
      summary: Free-text airport search
      description: Prefix-matches ident / ICAO / FAA LID / IATA and substring-matches name + municipality. Powers the viewer search box.
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string }
          example: KJFK
        - name: type
          in: query
          schema: { type: string }
        - name: country
          in: query
          schema: { type: string, minLength: 2, maxLength: 2 }
          description: ISO 3166-1 alpha-2 country filter.
        - name: authority
          in: query
          schema: { type: string }
          description: Source authority filter such as `faa` or `ourairports`.
        - { $ref: "#/components/parameters/limit" }
      responses:
        "200":
          description: Matching airport summaries.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/AirportSummary" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }

  /api/v1/airport/nearest:
    get:
      tags: [Airports, Derived]
      summary: Nearest airports to coordinates or an address
      description: |
        Returns airports ranked by exact great-circle distance, with the canonical
        `airport_id` plus ICAO, local and IATA identifiers when available. Provide
        exactly one origin: both `lat` and `lon`, or a one-line `address` with an
        optional two-letter `cc` country code.

        Coordinate lookups are mirror-only (`meta.upstream.status: skipped`). Address
        lookups resolve through the fleet `cca-geocode` service binding and preserve
        its source attribution (`meta.upstream.status: ok`). By default each result is
        an airport summary with a `detail_url`; `details=true` also embeds the existing
        full airport entity, including runways and frequencies when present.

        With no `radius`, the candidate window widens until enough nearest airports are
        found. An explicit `radius` filters results to that many nautical miles (capped
        at 1,000 NM).
      parameters:
        - name: lat
          in: query
          schema: { type: number, minimum: -90, maximum: 90 }
          description: Latitude in decimal degrees. Required with `lon` unless `address` is supplied.
          example: 40.7484
        - name: lon
          in: query
          schema: { type: number, minimum: -180, maximum: 180 }
          description: Longitude in decimal degrees. Required with `lat` unless `address` is supplied.
          example: -73.9857
        - name: address
          in: query
          schema: { type: string, maxLength: 500 }
          description: One-line street address. Mutually exclusive with `lat` / `lon`.
          example: 350 5th Ave, New York, NY
        - name: cc
          in: query
          schema: { type: string, minLength: 2, maxLength: 2, default: us }
          description: |
            ISO 3166-1 alpha-2 country code for `address`. Owned country indexes remain
            preferred by `cca-geocode`; other valid codes use its bounded
            OpenStreetMap/Nominatim fallback through the fleet MAPS service binding.
        - name: radius
          in: query
          schema: { type: number, exclusiveMinimum: 0, maximum: 1000 }
          description: Optional maximum great-circle distance in nautical miles.
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 25, default: 5 }
        - name: details
          in: query
          schema: { type: boolean, default: false }
          description: Embed each full airport entity in `result.details`.
      responses:
        "200":
          description: Distance-ranked airports.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties: { data: { $ref: "#/components/schemas/NearestAirports" } }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/ErrorEnvelopeResponse" }
        "501": { $ref: "#/components/responses/ErrorEnvelopeResponse" }
        "502": { $ref: "#/components/responses/ErrorEnvelopeResponse" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
        "504": { $ref: "#/components/responses/ErrorEnvelopeResponse" }

  /api/v1/airport/{id}:
    get:
      tags: [Airports]
      summary: Airport headline + runways
      description: |
        Resolve an airport by ICAO, IATA, national local id, or source ident. With `?as_of=`
        returns the state at a past cycle/date. US idents present in both FAA and OurAirports
        resolve to the FAA row; non-US idents (for example `EGLL`, IATA alias `LHR`, or
        OurAirports `DE-0411`) resolve globally. Optional `country` and `authority` filters
        disambiguate reused national identifiers. Per-airport radio
        `frequencies` (an OurAirports global product) are joined by canonical ICAO ident, so an
        FAA airport surfaces them too; the key is omitted only when none are on record.
      parameters:
        - $ref: "#/components/parameters/airportId"
        - $ref: "#/components/parameters/asOf"
        - $ref: "#/components/parameters/fields"
        - $ref: "#/components/parameters/refresh"
        - name: country
          in: query
          schema: { type: string, minLength: 2, maxLength: 2 }
          description: ISO 3166-1 alpha-2 disambiguation filter.
        - name: authority
          in: query
          schema: { type: string }
          description: Source-authority disambiguation filter.
      responses:
        "200":
          description: Airport headline.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties: { data: { $ref: "#/components/schemas/Airport" } }
        "404": { $ref: "#/components/responses/NotInMirror" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/airport/{id}/charts:
    get:
      tags: [Airports, Charts]
      summary: Airport d-TPP terminal procedures (charts & diagrams)
      description: |
        The airport's FAA **d-TPP** (digital Terminal Procedures) charts for the latest
        published cycle — airport diagram, approach (IAP), departure (DP/ODP), arrival
        (STAR), takeoff/alternate minimums, hot spots — grouped by type. Each chart links
        to the same-origin PDF serve route `/api/v1/charts/dtpp/{cycle}/{pdf_name}` (our
        own R2 mirror, never a live FAA proxy). Resolves the airport by ICAO ident **or**
        FAA LID. Mirror-only read: no upstream in the path; `count` is `0` with empty
        `groups` when the airport has no published procedures.
      parameters:
        - $ref: "#/components/parameters/airportId"
        - $ref: "#/components/parameters/asOf"
      responses:
        "200":
          description: Grouped terminal procedures for the airport.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties:
                      data:
                        type: object
                        properties:
                          airport:
                            type: object
                            properties:
                              id: { type: string }
                              icao_ident: { type: [string, "null"] }
                              airport_id: { type: [string, "null"] }
                              name: { type: [string, "null"] }
                          cycle: { type: [string, "null"], description: AIRAC-aligned YYMM cycle. }
                          effective_from: { type: [string, "null"], format: date }
                          effective_to: { type: [string, "null"], format: date }
                          count: { type: integer }
                          groups:
                            type: array
                            items:
                              type: object
                              properties:
                                type: { type: string }
                                label: { type: string }
                                charts:
                                  type: array
                                  items:
                                    type: object
                                    properties:
                                      chart_code: { type: [string, "null"] }
                                      chart_name: { type: [string, "null"] }
                                      chart_seq: { type: [integer, "null"] }
                                      pdf_name: { type: string }
                                      procuid: { type: [string, "null"] }
                                      amdt_num: { type: [string, "null"] }
                                      amdt_date: { type: [string, "null"] }
                                      url: { type: string, description: Same-origin PDF serve path. }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/airport/{id}/charts/cycles:
    get:
      tags: [Airports, Charts]
      summary: Airport d-TPP chart cycles (history index)
      description: |
        The FAA **d-TPP** cycles mirrored for this airport, newest first — each with its
        effective window and chart count. Powers the airport page's "As of" chart selector and
        lets a client time-travel via `/api/v1/airport/{id}/charts?as_of={cycle}`. Resolves the
        airport by ICAO ident **or** FAA LID. Mirror-only read: `cycles` is `[]` when none are
        mirrored.
      parameters:
        - $ref: "#/components/parameters/airportId"
      responses:
        "200":
          description: Mirrored d-TPP cycles for the airport, newest first.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties:
                      data:
                        type: object
                        properties:
                          airport:
                            type: object
                            properties:
                              id: { type: string }
                              icao_ident: { type: [string, "null"] }
                              airport_id: { type: [string, "null"] }
                              name: { type: [string, "null"] }
                          count: { type: integer }
                          cycles:
                            type: array
                            items:
                              type: object
                              properties:
                                cycle: { type: string, description: AIRAC-aligned YYMM cycle. }
                                effective_from: { type: [string, "null"], format: date }
                                effective_to: { type: [string, "null"], format: date }
                                count: { type: integer, description: Chart count in that cycle. }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/airport/{id}/procedures:
    get:
      tags: [Airports, Procedures]
      summary: Airport coded instrument procedures (CIFP / ARINC 424)
      description: |
        The airport's FAA **CIFP** (Coded Instrument Flight Procedures) for the latest
        published AIRAC cycle — the computer-readable SIDs, STARs and instrument
        approaches (IAPs) behind the d-TPP plate PDFs. Grouped by type (`SID` / `STAR` /
        `IAP`); each entry summarises the procedure ident, its transitions and a leg count,
        and links to the full per-procedure detail at
        `/api/v1/procedure/{airport}/{type}/{ident}`. Resolves the airport by ICAO ident
        **or** FAA LID. Mirror-only read: no upstream in the path; `count` is `0` with empty
        `groups` when the airport has no coded procedures.
      parameters:
        - $ref: "#/components/parameters/airportId"
        - $ref: "#/components/parameters/asOf"
      responses:
        "200":
          description: Grouped coded procedures for the airport.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties:
                      data:
                        type: object
                        properties:
                          airport:
                            type: object
                            properties:
                              id: { type: string }
                              icao_ident: { type: [string, "null"] }
                              name: { type: [string, "null"] }
                          cycle: { type: [string, "null"], description: AIRAC effective date (YYYY-MM-DD). }
                          effective_from: { type: [string, "null"], format: date }
                          effective_to: { type: [string, "null"], format: date }
                          count: { type: integer }
                          groups:
                            type: array
                            items:
                              type: object
                              properties:
                                type: { type: string, enum: [SID, STAR, IAP] }
                                label: { type: string }
                                count: { type: integer }
                                procedures:
                                  type: array
                                  items: { $ref: "#/components/schemas/ProcedureSummary" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/airport/{id}/procedures/cycles:
    get:
      tags: [Airports, Procedures]
      summary: Airport CIFP procedure cycles (history index)
      description: |
        The FAA **CIFP** cycles mirrored for this airport, newest first — each with its
        effective window and procedure count. Powers the airport page's "As of" procedures
        selector and lets a client time-travel via
        `/api/v1/airport/{id}/procedures?as_of={cycle}`. Resolves the airport by ICAO ident
        **or** FAA LID. Mirror-only read: `cycles` is `[]` when none are mirrored.
      parameters:
        - $ref: "#/components/parameters/airportId"
      responses:
        "200":
          description: Mirrored CIFP cycles for the airport, newest first.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties:
                      data:
                        type: object
                        properties:
                          airport:
                            type: object
                            properties:
                              id: { type: string }
                              icao_ident: { type: [string, "null"] }
                              name: { type: [string, "null"] }
                          count: { type: integer }
                          cycles:
                            type: array
                            items:
                              type: object
                              properties:
                                cycle: { type: string, description: AIRAC effective date (YYYY-MM-DD). }
                                effective_from: { type: [string, "null"], format: date }
                                effective_to: { type: [string, "null"], format: date }
                                count: { type: integer, description: Procedure count in that cycle. }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/cycles:
    get:
      tags: [Charts]
      summary: Global cycle index (which cycles are mirrored, per dataset)
      description: |
        The mirror's cycle bookkeeping — every ingested AIRAC / snapshot cycle, grouped by
        dataset (`dtpp`, `cifp`, `nasr`, `dof`, …), newest effective first. Lets a client
        discover what history exists before deep-linking a dataset / airport `as_of`. Optional
        `?dataset=` narrows to a single dataset id. Mirror-only read (no upstream, no PII):
        `datasets` is `[]` when nothing is mirrored.
      parameters:
        - name: dataset
          in: query
          required: false
          schema: { type: string }
          example: dtpp
          description: Filter to a single dataset id.
      responses:
        "200":
          description: Mirrored cycles grouped by dataset.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties:
                      data:
                        type: object
                        properties:
                          count: { type: integer }
                          datasets:
                            type: array
                            items:
                              type: object
                              properties:
                                dataset: { type: string }
                                authority: { type: string }
                                kind: { type: [string, "null"] }
                                count: { type: integer }
                                cycles:
                                  type: array
                                  items:
                                    type: object
                                    properties:
                                      effective: { type: string }
                                      kind: { type: [string, "null"] }
                                      ingested_at: { type: [string, "null"] }
  /api/v1/airport/{id}/routes:
    get:
      tags: [Airports]
      summary: Airline routes touching the airport (OpenFlights)
      description: |
        The OpenFlights scheduled routes that **originate or terminate** at this airport,
        split into `departures` (src = airport) and `arrivals` (dst = airport). The airport
        ident is resolved to every code it is known by (ICAO / FAA LID / IATA) via the
        facilities mirror, so a route flown under the IATA code (e.g. `JFK`) is matched even
        when you ask by ICAO ident (`KJFK`). Each route carries the operating carrier
        (resolved to its name), the endpoints, codeshare/stops flags and the equipment codes.
        Mirror-only read (no upstream in the path); ODbL — attribute "OpenFlights".
      parameters:
        - $ref: "#/components/parameters/airportId"
        - name: direction
          in: query
          schema: { type: string, enum: [departures, arrivals, both], default: both }
          description: Limit to one leg of the journey, or both.
        - name: limit
          in: query
          schema: { type: integer, default: 100 }
          description: Max routes returned per direction (1–1000).
      responses:
        "200":
          description: Routes grouped into departures and arrivals.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties: { data: { $ref: "#/components/schemas/AirportRoutes" } }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/airport/{id}/history:
    get:
      tags: [History & change]
      summary: Airport version history
      parameters:
        - $ref: "#/components/parameters/airportId"
        - $ref: "#/components/parameters/historyFormat"
        - $ref: "#/components/parameters/seqFrom"
        - $ref: "#/components/parameters/seqTo"
        - $ref: "#/components/parameters/limit"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airport/{id}/changes:
    get:
      tags: [History & change]
      summary: Airport field-level changes
      parameters:
        - $ref: "#/components/parameters/airportId"
        - $ref: "#/components/parameters/since"
        - $ref: "#/components/parameters/until"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airport/{id}/raw:
    get:
      tags: [History & change]
      summary: Airport raw-archive pointers
      parameters: [{ $ref: "#/components/parameters/airportId" }]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airport/{id}/raw/{cycle}:
    get:
      tags: [History & change]
      summary: Airport raw-archive pointer for one cycle
      parameters:
        - $ref: "#/components/parameters/airportId"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airport/{id}/snapshots:
    get:
      tags: [History & change]
      summary: List stored airport snapshots
      parameters: [{ $ref: "#/components/parameters/airportId" }]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airport/{id}/snapshots/{cycle}:
    get:
      tags: [History & change]
      summary: Airport snapshot at a cycle
      parameters:
        - $ref: "#/components/parameters/airportId"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airport/{id}/compare:
    get:
      tags: [History & change]
      summary: Compare an airport across two cycles
      parameters:
        - $ref: "#/components/parameters/airportId"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }

  # ---- Reference lists (OurAirports countries / regions — mirror-only) ----
  /api/v1/country/{code}:
    get:
      tags: [Reference]
      summary: ISO 3166-1 country (OurAirports)
      description: |
        Resolve a country from the OurAirports `countries.csv` reference list by ISO 3166-1
        alpha-2 code. Mirror-only (no history); `meta.cycle.type` is `snapshot`.
      parameters:
        - name: code
          in: path
          required: true
          schema: { type: string, example: GB }
          description: ISO 3166-1 alpha-2 country code.
        - $ref: "#/components/parameters/fields"
        - $ref: "#/components/parameters/refresh"
      responses:
        "200":
          description: Country reference record.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties: { data: { $ref: "#/components/schemas/Country" } }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }

  /api/v1/region/{code}:
    get:
      tags: [Reference]
      summary: ISO 3166-2 region (OurAirports)
      description: |
        Resolve a region from the OurAirports `regions.csv` reference list by ISO 3166-2
        code (e.g. `US-PA`). Mirror-only (no history); `meta.cycle.type` is `snapshot`.
      parameters:
        - name: code
          in: path
          required: true
          schema: { type: string, example: US-PA }
          description: ISO 3166-2 region code.
        - $ref: "#/components/parameters/fields"
        - $ref: "#/components/parameters/refresh"
      responses:
        "200":
          description: Region reference record.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties: { data: { $ref: "#/components/schemas/Region" } }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }

  # ---- Airlines (OpenFlights global feed) ----
  /api/v1/airline/search:
    get:
      tags: [Reference]
      summary: Search airlines (OpenFlights)
      description: |
        Free-text search over the OpenFlights airlines by name, IATA / ICAO code or callsign;
        exact code matches rank first and active carriers ahead of defunct ones. Optional
        `country` (operating-country substring) and `active` (`Y`/`N`) filters. Mirror-only;
        ODbL — attribute "OpenFlights".
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, example: american }, description: Name / IATA / ICAO / callsign query. }
        - { name: country, in: query, schema: { type: string, example: United States }, description: Operating-country substring filter. }
        - { name: active, in: query, schema: { type: string, enum: [Y, N] }, description: Filter by active flag. }
        - $ref: "#/components/parameters/limit"
      responses:
        "200":
          description: Matching airlines (ranked).
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties: { data: { type: array, items: { $ref: "#/components/schemas/Airline" } } }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }

  /api/v1/airline/{code}:
    get:
      tags: [Reference]
      summary: Airline by IATA or ICAO code (OpenFlights)
      description: |
        Resolve an OpenFlights airline by its 2-char **IATA** or 3-char **ICAO** code. IATA
        codes are not unique across carriers, so when several match an ICAO-exact hit wins,
        then an active carrier, then the lowest AirlineID — deterministic and stable.
        Mirror-only (no history); `meta.cycle.type` is `snapshot`. ODbL — attribute "OpenFlights".
      parameters:
        - name: code
          in: path
          required: true
          schema: { type: string, example: AAL }
          description: 2-char IATA or 3-char ICAO airline code.
        - $ref: "#/components/parameters/fields"
        - $ref: "#/components/parameters/refresh"
      responses:
        "200":
          description: Airline record.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties: { data: { $ref: "#/components/schemas/Airline" } }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }

  # ---- Navigation: NASR navaids / fixes / airways (nav-data foundation) ----
  /api/v1/navaids.geojson:
    get:
      tags: [Map, Navigation]
      summary: Navaids vector overlay (raw GeoJSON)
      description: |
        A raw `application/geo+json` **FeatureCollection** of mirrored navaids (VOR/DME/
        TACAN/NDB) for the map overlay. Like `/airports.geojson` this is **not** wrapped in
        the fleet envelope — MapLibre needs a bare FeatureCollection. Mirror-only; no PII.
      parameters:
        - name: bbox
          in: query
          schema: { type: string }
          example: "-74.3,40.4,-73.5,40.9"
          description: Clip to `w,s,e,n`; `w > e` crosses the antimeridian.
        - name: type
          in: query
          schema: { type: string }
          example: VOR/DME
          description: Filter by navaid type.
        - name: authority
          in: query
          schema: { type: string }
          description: Source authority filter. Omit to include every mirrored authority.
        - name: country
          in: query
          schema: { type: string, example: US }
        - name: limit
          in: query
          schema: { type: integer, default: 5000, maximum: 20000 }
        - $ref: "#/components/parameters/refresh"
      responses:
        "200":
          description: A GeoJSON FeatureCollection of navaids.
          headers:
            X-Cycle: { schema: { type: string }, description: Newest cycle represented. }
            X-Source: { schema: { type: string }, description: Comma-separated provenance for returned authorities. }
            X-Cache: { schema: { type: string, enum: [hit, miss, bypass] } }
          content:
            application/geo+json:
              schema: { $ref: "#/components/schemas/NavaidFeatureCollection" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/fixes.geojson:
    get:
      tags: [Map, Navigation]
      summary: Fixes vector overlay (raw GeoJSON)
      description: |
        A raw `application/geo+json` **FeatureCollection** of mirrored fixes / waypoints.
        Not wrapped in the fleet envelope (MapLibre needs a bare FeatureCollection).
        Mirror-only; no PII.
      parameters:
        - name: bbox
          in: query
          schema: { type: string }
          example: "-74.3,40.4,-73.5,40.9"
          description: Clip to `w,s,e,n`; `w > e` crosses the antimeridian.
        - name: authority
          in: query
          schema: { type: string }
          description: Source authority filter. Omit to include every mirrored authority.
        - name: country
          in: query
          schema: { type: string, example: US }
        - name: limit
          in: query
          schema: { type: integer, default: 5000, maximum: 20000 }
        - $ref: "#/components/parameters/refresh"
      responses:
        "200":
          description: A GeoJSON FeatureCollection of fixes.
          headers:
            X-Cycle: { schema: { type: string }, description: Newest cycle represented. }
            X-Source: { schema: { type: string }, description: Comma-separated provenance for returned authorities. }
            X-Cache: { schema: { type: string, enum: [hit, miss, bypass] } }
          content:
            application/geo+json:
              schema: { $ref: "#/components/schemas/FixFeatureCollection" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/airways.geojson:
    get:
      tags: [Map, Navigation]
      summary: Airways vector overlay (raw GeoJSON LineStrings)
      description: |
        A raw `application/geo+json` **FeatureCollection** of mirrored airways as
        **LineString** geometries, resolved by joining each leg's fix/navaid name to its
        coordinates. Airways whose vertices cannot be resolved to <2 points are omitted.
        Not wrapped in the fleet envelope. Mirror-only; no PII.
      parameters:
        - name: bbox
          in: query
          schema: { type: string }
          description: Clip to `w,s,e,n` (any vertex inside); `w > e` crosses the antimeridian.
        - name: authority
          in: query
          schema: { type: string }
          description: Source authority filter. Omit to include every mirrored authority.
        - name: country
          in: query
          schema: { type: string, example: US }
        - name: limit
          in: query
          schema: { type: integer, default: 5000, maximum: 20000 }
        - $ref: "#/components/parameters/refresh"
      responses:
        "200":
          description: A GeoJSON FeatureCollection of airway LineStrings.
          headers:
            X-Cycle: { schema: { type: string }, description: Newest cycle represented. }
            X-Source: { schema: { type: string }, description: Comma-separated provenance for returned authorities. }
            X-Cache: { schema: { type: string, enum: [hit, miss, bypass] } }
          content:
            application/geo+json:
              schema: { $ref: "#/components/schemas/AirwayFeatureCollection" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }

  /api/v1/navaid/search:
    get:
      tags: [Map, Navigation]
      summary: Free-text navaid search
      description: Prefix-matches ident and substring-matches name; exact ident ranks first.
      parameters:
        - { name: q, in: query, required: true, schema: { type: string }, example: JFK }
        - { name: type, in: query, schema: { type: string } }
        - { name: country, in: query, schema: { type: string } }
        - { $ref: "#/components/parameters/limit" }
      responses:
        "200":
          description: Matching navaid summaries.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/NavaidSummary" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/fix/search:
    get:
      tags: [Map, Navigation]
      summary: Free-text fix search
      description: Prefix-matches the fix name; exact name ranks first.
      parameters:
        - { name: q, in: query, required: true, schema: { type: string }, example: CEVIC }
        - { name: country, in: query, schema: { type: string } }
        - { $ref: "#/components/parameters/limit" }
      responses:
        "200":
          description: Matching fix summaries.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/FixSummary" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/airway/search:
    get:
      tags: [Map, Navigation]
      summary: Free-text airway search
      description: Prefix-matches the airway designator; exact designator ranks first.
      parameters:
        - { name: q, in: query, required: true, schema: { type: string }, example: V25 }
        - { name: country, in: query, schema: { type: string } }
        - { $ref: "#/components/parameters/limit" }
      responses:
        "200":
          description: Matching airway summaries.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/AirwaySummary" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }

  /api/v1/navaid/{ident}:
    get:
      tags: [Navigation]
      summary: Navaid headline (+ co-located types)
      description: Resolve a navaid by ident. Headline carries the primary type; co-located extra types sharing the ident are bundled under `colocated`. With `?as_of=` returns a past cycle.
      parameters:
        - $ref: "#/components/parameters/navaidIdent"
        - $ref: "#/components/parameters/asOf"
        - $ref: "#/components/parameters/fields"
        - $ref: "#/components/parameters/refresh"
      responses:
        "200":
          description: Navaid headline.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties: { data: { $ref: "#/components/schemas/Navaid" } }
        "404": { $ref: "#/components/responses/NotInMirror" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/navaid/{ident}/history:
    get:
      tags: [History & change]
      summary: Navaid version history
      parameters:
        - $ref: "#/components/parameters/navaidIdent"
        - $ref: "#/components/parameters/historyFormat"
        - $ref: "#/components/parameters/seqFrom"
        - $ref: "#/components/parameters/seqTo"
        - $ref: "#/components/parameters/limit"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/navaid/{ident}/changes:
    get:
      tags: [History & change]
      summary: Navaid field-level changes
      parameters:
        - $ref: "#/components/parameters/navaidIdent"
        - $ref: "#/components/parameters/since"
        - $ref: "#/components/parameters/until"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/navaid/{ident}/raw:
    get:
      tags: [History & change]
      summary: Navaid raw-archive pointers
      parameters: [{ $ref: "#/components/parameters/navaidIdent" }]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/navaid/{ident}/raw/{cycle}:
    get:
      tags: [History & change]
      summary: Navaid raw-archive pointer for one cycle
      parameters:
        - $ref: "#/components/parameters/navaidIdent"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/navaid/{ident}/snapshots:
    get:
      tags: [History & change]
      summary: List stored navaid snapshots
      parameters: [{ $ref: "#/components/parameters/navaidIdent" }]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/navaid/{ident}/snapshots/{cycle}:
    get:
      tags: [History & change]
      summary: Navaid snapshot at a cycle
      parameters:
        - $ref: "#/components/parameters/navaidIdent"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/navaid/{ident}/compare:
    get:
      tags: [History & change]
      summary: Compare a navaid across two cycles
      parameters:
        - $ref: "#/components/parameters/navaidIdent"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }

  /api/v1/fix/{name}:
    get:
      tags: [Navigation]
      summary: Fix headline
      description: Resolve a fix / waypoint by name. With `?as_of=` returns a past cycle.
      parameters:
        - $ref: "#/components/parameters/fixName"
        - $ref: "#/components/parameters/asOf"
        - $ref: "#/components/parameters/fields"
        - $ref: "#/components/parameters/refresh"
      responses:
        "200":
          description: Fix headline.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties: { data: { $ref: "#/components/schemas/Fix" } }
        "404": { $ref: "#/components/responses/NotInMirror" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/fix/{name}/history:
    get:
      tags: [History & change]
      summary: Fix version history
      parameters:
        - $ref: "#/components/parameters/fixName"
        - $ref: "#/components/parameters/historyFormat"
        - $ref: "#/components/parameters/seqFrom"
        - $ref: "#/components/parameters/seqTo"
        - $ref: "#/components/parameters/limit"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/fix/{name}/changes:
    get:
      tags: [History & change]
      summary: Fix field-level changes
      parameters:
        - $ref: "#/components/parameters/fixName"
        - $ref: "#/components/parameters/since"
        - $ref: "#/components/parameters/until"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/fix/{name}/raw:
    get:
      tags: [History & change]
      summary: Fix raw-archive pointers
      parameters: [{ $ref: "#/components/parameters/fixName" }]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/fix/{name}/raw/{cycle}:
    get:
      tags: [History & change]
      summary: Fix raw-archive pointer for one cycle
      parameters:
        - $ref: "#/components/parameters/fixName"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/fix/{name}/snapshots:
    get:
      tags: [History & change]
      summary: List stored fix snapshots
      parameters: [{ $ref: "#/components/parameters/fixName" }]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/fix/{name}/snapshots/{cycle}:
    get:
      tags: [History & change]
      summary: Fix snapshot at a cycle
      parameters:
        - $ref: "#/components/parameters/fixName"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/fix/{name}/compare:
    get:
      tags: [History & change]
      summary: Compare a fix across two cycles
      parameters:
        - $ref: "#/components/parameters/fixName"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }

  /api/v1/airway/{designator}:
    get:
      tags: [Navigation]
      summary: Airway headline (+ ordered legs)
      description: Resolve an airway by designator with its ordered fix-to-fix legs. With `?as_of=` returns a past cycle.
      parameters:
        - $ref: "#/components/parameters/airwayDesignator"
        - $ref: "#/components/parameters/asOf"
        - $ref: "#/components/parameters/fields"
        - $ref: "#/components/parameters/refresh"
      responses:
        "200":
          description: Airway headline.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties: { data: { $ref: "#/components/schemas/Airway" } }
        "404": { $ref: "#/components/responses/NotInMirror" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/airway/{designator}/history:
    get:
      tags: [History & change]
      summary: Airway version history
      parameters:
        - $ref: "#/components/parameters/airwayDesignator"
        - $ref: "#/components/parameters/historyFormat"
        - $ref: "#/components/parameters/seqFrom"
        - $ref: "#/components/parameters/seqTo"
        - $ref: "#/components/parameters/limit"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airway/{designator}/changes:
    get:
      tags: [History & change]
      summary: Airway field-level changes
      parameters:
        - $ref: "#/components/parameters/airwayDesignator"
        - $ref: "#/components/parameters/since"
        - $ref: "#/components/parameters/until"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airway/{designator}/raw:
    get:
      tags: [History & change]
      summary: Airway raw-archive pointers
      parameters: [{ $ref: "#/components/parameters/airwayDesignator" }]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airway/{designator}/raw/{cycle}:
    get:
      tags: [History & change]
      summary: Airway raw-archive pointer for one cycle
      parameters:
        - $ref: "#/components/parameters/airwayDesignator"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airway/{designator}/snapshots:
    get:
      tags: [History & change]
      summary: List stored airway snapshots
      parameters: [{ $ref: "#/components/parameters/airwayDesignator" }]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airway/{designator}/snapshots/{cycle}:
    get:
      tags: [History & change]
      summary: Airway snapshot at a cycle
      parameters:
        - $ref: "#/components/parameters/airwayDesignator"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airway/{designator}/compare:
    get:
      tags: [History & change]
      summary: Compare an airway across two cycles
      parameters:
        - $ref: "#/components/parameters/airwayDesignator"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }

  /api/v1/obstacles.geojson:
    get:
      tags: [Map, Obstacles]
      summary: Obstacles vector overlay (raw GeoJSON)
      description: |
        A raw `application/geo+json` **FeatureCollection** of mirrored DOF obstacles
        (towers, antennas, buildings …) as **Point** features for the map overlay. Like
        the other overlays this is **not** wrapped in the fleet envelope — MapLibre needs a
        bare FeatureCollection. Mirror-only; no PII.
      parameters:
        - name: bbox
          in: query
          schema: { type: string }
          example: "-77.2,38.7,-76.9,39.0"
          description: Clip to `w,s,e,n`; `w > e` crosses the antimeridian.
        - name: type
          in: query
          schema: { type: string }
          example: TOWER
          description: Filter by obstacle type.
        - name: authority
          in: query
          schema: { type: string }
          description: Source authority filter. Omit to include every mirrored authority.
        - name: country
          in: query
          schema: { type: string, example: US }
        - name: limit
          in: query
          schema: { type: integer, default: 5000, maximum: 20000 }
        - $ref: "#/components/parameters/refresh"
      responses:
        "200":
          description: A GeoJSON FeatureCollection of obstacles.
          headers:
            X-Cycle: { schema: { type: string }, description: Newest cycle represented. }
            X-Source: { schema: { type: string }, description: Comma-separated provenance for returned authorities. }
            X-Cache: { schema: { type: string, enum: [hit, miss, bypass] } }
          content:
            application/geo+json:
              schema: { $ref: "#/components/schemas/ObstacleFeatureCollection" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/obstacle/{id}:
    get:
      tags: [Obstacles]
      summary: Obstacle headline (FAA Digital Obstacle File)
      description: |
        Resolve a single DOF obstacle by its canonical id `{OAS}-{number}` (e.g.
        `11-024069`). Headline carries position, AMSL/AGL height, type, lighting, marking,
        accuracy and verification status. With `?as_of=` returns a past cycle. The cycle is
        reported in `meta.cycle`. Mirror-only; no PII.
      parameters:
        - $ref: "#/components/parameters/obstacleId"
        - $ref: "#/components/parameters/asOf"
        - $ref: "#/components/parameters/fields"
        - $ref: "#/components/parameters/refresh"
      responses:
        "200":
          description: Obstacle headline.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties: { data: { $ref: "#/components/schemas/Obstacle" } }
        "404": { $ref: "#/components/responses/NotInMirror" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/obstacle/{id}/history:
    get:
      tags: [History & change]
      summary: Obstacle version history
      parameters:
        - $ref: "#/components/parameters/obstacleId"
        - $ref: "#/components/parameters/historyFormat"
        - $ref: "#/components/parameters/seqFrom"
        - $ref: "#/components/parameters/seqTo"
        - $ref: "#/components/parameters/limit"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/obstacle/{id}/changes:
    get:
      tags: [History & change]
      summary: Obstacle field-level changes
      parameters:
        - $ref: "#/components/parameters/obstacleId"
        - $ref: "#/components/parameters/since"
        - $ref: "#/components/parameters/until"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/obstacle/{id}/raw:
    get:
      tags: [History & change]
      summary: Obstacle raw-archive pointers
      parameters: [{ $ref: "#/components/parameters/obstacleId" }]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/obstacle/{id}/raw/{cycle}:
    get:
      tags: [History & change]
      summary: Obstacle raw-archive pointer for one cycle
      parameters:
        - $ref: "#/components/parameters/obstacleId"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/obstacle/{id}/snapshots:
    get:
      tags: [History & change]
      summary: List stored obstacle snapshots
      parameters: [{ $ref: "#/components/parameters/obstacleId" }]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/obstacle/{id}/snapshots/{cycle}:
    get:
      tags: [History & change]
      summary: Obstacle snapshot at a cycle
      parameters:
        - $ref: "#/components/parameters/obstacleId"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/obstacle/{id}/compare:
    get:
      tags: [History & change]
      summary: Compare an obstacle across two cycles
      parameters:
        - $ref: "#/components/parameters/obstacleId"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }

  /api/v1/procedure/{airport}/{type}/{ident}:
    get:
      tags: [Procedures]
      summary: Coded instrument procedure detail (CIFP / ARINC 424)
      description: |
        Resolve one coded instrument procedure by its compound id
        `{ICAO}/{type}/{ident}` (e.g. `KJFK/SID/DEEZZ6`), where `type` is `SID`, `STAR` or
        `IAP`. Returns the ordered ARINC 424 legs grouped by transition — runway
        transitions (`RW04L`), enroute transitions (`CANDR`), and the common segment —
        each leg carrying its path-and-termination (with a human label), altitude /
        speed / course constraints, turn direction, along-track distance and any
        recommended navaid. Leg fixes are referenced by ident + section + region; lat/lon
        are intentionally **not** invented here (a future overlay resolves them from the
        NASR fix/navaid mirror). With `?as_of=` returns a past cycle; the AIRAC cycle is
        reported in `meta.cycle`. Mirror-only; no PII.
      parameters:
        - $ref: "#/components/parameters/procedureAirport"
        - $ref: "#/components/parameters/procedureType"
        - $ref: "#/components/parameters/procedureIdent"
        - $ref: "#/components/parameters/asOf"
        - $ref: "#/components/parameters/fields"
        - $ref: "#/components/parameters/refresh"
      responses:
        "200":
          description: Coded procedure detail with ordered legs per transition.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties: { data: { $ref: "#/components/schemas/Procedure" } }
        "404": { $ref: "#/components/responses/NotInMirror" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/procedure/{airport}/{type}/{ident}/history:
    get:
      tags: [History & change]
      summary: Procedure version history
      parameters:
        - $ref: "#/components/parameters/procedureAirport"
        - $ref: "#/components/parameters/procedureType"
        - $ref: "#/components/parameters/procedureIdent"
        - $ref: "#/components/parameters/historyFormat"
        - $ref: "#/components/parameters/seqFrom"
        - $ref: "#/components/parameters/seqTo"
        - $ref: "#/components/parameters/limit"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/procedure/{airport}/{type}/{ident}/changes:
    get:
      tags: [History & change]
      summary: Procedure field-level changes
      parameters:
        - $ref: "#/components/parameters/procedureAirport"
        - $ref: "#/components/parameters/procedureType"
        - $ref: "#/components/parameters/procedureIdent"
        - $ref: "#/components/parameters/since"
        - $ref: "#/components/parameters/until"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/procedure/{airport}/{type}/{ident}/raw:
    get:
      tags: [History & change]
      summary: Procedure raw-archive pointers
      parameters:
        - $ref: "#/components/parameters/procedureAirport"
        - $ref: "#/components/parameters/procedureType"
        - $ref: "#/components/parameters/procedureIdent"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/procedure/{airport}/{type}/{ident}/raw/{cycle}:
    get:
      tags: [History & change]
      summary: Procedure raw-archive pointer for one cycle
      parameters:
        - $ref: "#/components/parameters/procedureAirport"
        - $ref: "#/components/parameters/procedureType"
        - $ref: "#/components/parameters/procedureIdent"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/procedure/{airport}/{type}/{ident}/snapshots:
    get:
      tags: [History & change]
      summary: List stored procedure snapshots
      parameters:
        - $ref: "#/components/parameters/procedureAirport"
        - $ref: "#/components/parameters/procedureType"
        - $ref: "#/components/parameters/procedureIdent"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/procedure/{airport}/{type}/{ident}/snapshots/{cycle}:
    get:
      tags: [History & change]
      summary: Procedure snapshot at a cycle
      parameters:
        - $ref: "#/components/parameters/procedureAirport"
        - $ref: "#/components/parameters/procedureType"
        - $ref: "#/components/parameters/procedureIdent"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/procedure/{airport}/{type}/{ident}/compare:
    get:
      tags: [History & change]
      summary: Compare a procedure across two cycles
      parameters:
        - $ref: "#/components/parameters/procedureAirport"
        - $ref: "#/components/parameters/procedureType"
        - $ref: "#/components/parameters/procedureIdent"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }

  /api/v1/aircraft/{mark}:
    get:
      tags: [Aircraft]
      summary: Aircraft registration headline
      description: N-number registry headline (owner, model, engine, status, ICAO hex). **Carries PII.**
      parameters:
        - $ref: "#/components/parameters/aircraftMark"
        - $ref: "#/components/parameters/asOf"
        - $ref: "#/components/parameters/fields"
        - $ref: "#/components/parameters/refresh"
      responses:
        "200":
          description: Aircraft headline.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties: { data: { $ref: "#/components/schemas/AircraftRegistration" } }
        "404": { $ref: "#/components/responses/NotInMirror" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/aircraft/search:
    get:
      tags: [Aircraft]
      summary: Search the registry
      description: Filter by owner, manufacturer/make, ICAO hex, model code or status. At least one filter is required (a bare call 400s — anti-enumeration). **Carries PII.**
      parameters:
        - { name: owner, in: query, schema: { type: string }, description: Registrant / owner name prefix. }
        - { name: mfr, in: query, schema: { type: string }, description: "Manufacturer / make name prefix (FAA ACFTREF MFR, e.g. CESSNA). Alias: make." }
        - { name: make, in: query, schema: { type: string }, description: Alias for mfr. }
        - { name: hex, in: query, schema: { type: string }, description: ICAO 24-bit hex. }
        - { name: model, in: query, schema: { type: string }, description: FAA make/model code prefix. }
        - { name: status, in: query, schema: { type: string } }
        - { $ref: "#/components/parameters/limit" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/aircraft/models/{code}:
    get:
      tags: [Aircraft]
      summary: Aircraft make/model reference
      parameters:
        - { name: code, in: path, required: true, schema: { type: string }, description: FAA make/model code. }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/aircraft/{mark}/history:
    get:
      tags: [History & change]
      summary: Aircraft version history (registration + ownership)
      parameters:
        - $ref: "#/components/parameters/aircraftMark"
        - $ref: "#/components/parameters/historyFormat"
        - $ref: "#/components/parameters/seqFrom"
        - $ref: "#/components/parameters/seqTo"
        - $ref: "#/components/parameters/limit"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/aircraft/{mark}/changes:
    get:
      tags: [History & change]
      summary: Aircraft field-level changes
      parameters:
        - $ref: "#/components/parameters/aircraftMark"
        - $ref: "#/components/parameters/since"
        - $ref: "#/components/parameters/until"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/aircraft/{mark}/raw:
    get:
      tags: [History & change]
      summary: Aircraft raw-archive pointers
      parameters: [{ $ref: "#/components/parameters/aircraftMark" }]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/aircraft/{mark}/raw/{cycle}:
    get:
      tags: [History & change]
      summary: Aircraft raw-archive pointer for one cycle
      parameters:
        - $ref: "#/components/parameters/aircraftMark"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/aircraft/{mark}/snapshots:
    get:
      tags: [History & change]
      summary: List stored aircraft snapshots
      parameters: [{ $ref: "#/components/parameters/aircraftMark" }]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/aircraft/{mark}/snapshots/{cycle}:
    get:
      tags: [History & change]
      summary: Aircraft snapshot at a cycle
      parameters:
        - $ref: "#/components/parameters/aircraftMark"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/aircraft/{mark}/compare:
    get:
      tags: [History & change]
      summary: Compare an aircraft across two cycles
      parameters:
        - $ref: "#/components/parameters/aircraftMark"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }

  /api/v1/airman/{id}:
    get:
      tags: [Airmen]
      summary: Airman certification headline
      description: Certificates + ratings for a FAA UNIQUE ID. **Carries PII.**
      parameters:
        - $ref: "#/components/parameters/airmanId"
        - $ref: "#/components/parameters/asOf"
        - $ref: "#/components/parameters/fields"
        - $ref: "#/components/parameters/refresh"
      responses:
        "200":
          description: Airman headline.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties: { data: { $ref: "#/components/schemas/AirmanCertification" } }
        "404": { $ref: "#/components/responses/NotInMirror" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/airman/search:
    get:
      tags: [Airmen]
      summary: Search airmen
      description: Filter by last name, first name, state or certificate type. **Carries PII.**
      parameters:
        - { name: name, in: query, schema: { type: string }, description: Last-name prefix. }
        - { name: first, in: query, schema: { type: string }, description: First-name prefix. }
        - { name: cert, in: query, schema: { type: string }, description: Certificate type. }
        - { name: state, in: query, schema: { type: string } }
        - { name: mci, in: query, schema: { type: string, pattern: "^[a-z0-9]{4}-[a-z0-9]{12}$" },
            description: Search by CCA identity — resolves the MCI through `mci_links` and returns each linked airman (with the `cca` block). Ignores the other filters. }
        - { $ref: "#/components/parameters/limit" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/mci/{mci}:
    get:
      tags: [Airmen]
      summary: CCA MCI reverse lookup
      description: |
        Every FAA record linked to one CCA **Matrix Consumer Identifier** — airman
        certifications and aircraft operator registrations, grouped by type. MCIs are
        minted by cca-admin (the fleet identity authority); this worker only stores the
        links (aero-core `mci_links`, the same read-time CCA-local layer as
        `cca-override` — the mirror stays pure upstream). A valid MCI with no links
        returns empty lists, not a 404. **Points at PII** — private/WAF-gated like the
        airman/aircraft reads it references.
      parameters:
        - { name: mci, in: path, required: true, schema: { type: string, pattern: "^[a-z0-9]{4}-[a-z0-9]{12}$" }, example: cgxc-2d82864bfda4 }
      responses:
        "200":
          description: Linked FAA records.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties: { data: { $ref: "#/components/schemas/MciIdentity" } }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/airman/{id}/history:
    get:
      tags: [History & change]
      summary: Airman version history
      parameters:
        - $ref: "#/components/parameters/airmanId"
        - $ref: "#/components/parameters/historyFormat"
        - $ref: "#/components/parameters/seqFrom"
        - $ref: "#/components/parameters/seqTo"
        - $ref: "#/components/parameters/limit"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airman/{id}/changes:
    get:
      tags: [History & change]
      summary: Airman field-level changes
      parameters:
        - $ref: "#/components/parameters/airmanId"
        - $ref: "#/components/parameters/since"
        - $ref: "#/components/parameters/until"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airman/{id}/raw:
    get:
      tags: [History & change]
      summary: Airman raw-archive pointers
      parameters: [{ $ref: "#/components/parameters/airmanId" }]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airman/{id}/raw/{cycle}:
    get:
      tags: [History & change]
      summary: Airman raw-archive pointer for one cycle
      parameters:
        - $ref: "#/components/parameters/airmanId"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airman/{id}/snapshots:
    get:
      tags: [History & change]
      summary: List stored airman snapshots
      parameters: [{ $ref: "#/components/parameters/airmanId" }]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airman/{id}/snapshots/{cycle}:
    get:
      tags: [History & change]
      summary: Airman snapshot at a cycle
      parameters:
        - $ref: "#/components/parameters/airmanId"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/airman/{id}/compare:
    get:
      tags: [History & change]
      summary: Compare an airman across two cycles
      parameters:
        - $ref: "#/components/parameters/airmanId"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }

  /api/v1/charts:
    get:
      tags: [Charts]
      summary: Chart product catalog
      description: Every chart product's current edition. Each carries faacharts tile/TileJSON links, bounds, zoom and attribution — everything a viewer needs.
      responses:
        "200":
          description: Chart products.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/ChartProduct" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/charts/dtpp/{cycle}/{pdf_name}:
    get:
      tags: [Charts]
      summary: Stream a mirrored d-TPP terminal-procedure PDF
      description: |
        Serve one FAA **d-TPP** PDF (airport diagram or approach/departure/arrival plate)
        from our own R2 mirror. **Own-it-forever:** an R2 hit streams with no upstream
        (`X-Cache-Status: hit`, `X-Upstream-Status: skipped`); on a miss the PDF is
        (feature-flag-gated) fetched once from FAA, archived write-once, then streamed —
        never a dead link. **Security:** only a `pdf_name` present in the cycle's index is
        served; this is a mirror, never an open FAA proxy. The response is the raw PDF
        (`application/pdf`), not the fleet JSON envelope.
      parameters:
        - { name: cycle, in: path, required: true, schema: { type: string, pattern: "^[0-9]{4}$" }, description: AIRAC-aligned YYMM cycle. }
        - { name: pdf_name, in: path, required: true, schema: { type: string }, description: Upstream PDF filename (uppercase, e.g. 00610AD.PDF). }
      responses:
        "200":
          description: The PDF bytes.
          content:
            application/pdf:
              schema: { type: string, format: binary }
        "400": { $ref: "#/components/responses/ErrorEnvelopeResponse" }
        "404": { $ref: "#/components/responses/NotInMirror" }
        "502": { $ref: "#/components/responses/ErrorEnvelopeResponse" }
        "503": { $ref: "#/components/responses/ErrorEnvelopeResponse" }
  /api/v1/charts/{product}:
    get:
      tags: [Charts]
      summary: Chart product detail
      description: |
        Current-edition detail (georef + sheets + tile links) for one product. With
        `?as_of=` returns a past edition. An authority-namespaced form
        `/api/v1/charts/{authority}/{product}` is also accepted to disambiguate a product
        slug shared across authorities.
      parameters:
        - $ref: "#/components/parameters/chartProduct"
        - $ref: "#/components/parameters/asOf"
        - $ref: "#/components/parameters/fields"
      responses:
        "200":
          description: Chart product detail.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties: { data: { $ref: "#/components/schemas/ChartProduct" } }
        "404": { $ref: "#/components/responses/NotInMirror" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
  /api/v1/charts/{product}/{cycle}:
    get:
      tags: [Charts]
      summary: Chart product edition pinned to a cycle
      parameters:
        - $ref: "#/components/parameters/chartProduct"
        - $ref: "#/components/parameters/cyclePath"
        - $ref: "#/components/parameters/fields"
      responses:
        "200":
          description: Pinned chart edition.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties: { data: { $ref: "#/components/schemas/ChartProduct" } }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/charts/{product}/history:
    get:
      tags: [History & change]
      summary: Chart product edition history
      parameters:
        - $ref: "#/components/parameters/chartProduct"
        - $ref: "#/components/parameters/historyFormat"
        - $ref: "#/components/parameters/seqFrom"
        - $ref: "#/components/parameters/seqTo"
        - $ref: "#/components/parameters/limit"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/charts/{product}/changes:
    get:
      tags: [History & change]
      summary: Chart product field-level changes
      parameters:
        - $ref: "#/components/parameters/chartProduct"
        - $ref: "#/components/parameters/since"
        - $ref: "#/components/parameters/until"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/charts/{product}/raw:
    get:
      tags: [History & change]
      summary: Chart product raw-archive pointers
      parameters: [{ $ref: "#/components/parameters/chartProduct" }]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/charts/{product}/raw/{cycle}:
    get:
      tags: [History & change]
      summary: Chart product raw-archive pointer for one cycle
      parameters:
        - $ref: "#/components/parameters/chartProduct"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/charts/{product}/snapshots:
    get:
      tags: [History & change]
      summary: List stored chart product snapshots
      parameters: [{ $ref: "#/components/parameters/chartProduct" }]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/charts/{product}/snapshots/{cycle}:
    get:
      tags: [History & change]
      summary: Chart product snapshot at a cycle
      parameters:
        - $ref: "#/components/parameters/chartProduct"
        - $ref: "#/components/parameters/cyclePath"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/charts/{product}/compare:
    get:
      tags: [History & change]
      summary: Compare a chart product across two editions
      parameters:
        - $ref: "#/components/parameters/chartProduct"
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }

  /api/v1/changes:
    get:
      tags: [Global]
      summary: Global cross-entity change feed
      description: What changed across every collection, newest first, cursor-paginated.
      parameters:
        - $ref: "#/components/parameters/since"
        - $ref: "#/components/parameters/until"
        - { name: authority, in: query, schema: { type: string } }
        - { name: country, in: query, schema: { type: string } }
        - { name: entity_type, in: query, schema: { type: string, enum: [airport, aircraft, airman, chart] } }
        - { $ref: "#/components/parameters/limit" }
        - { name: cursor, in: query, schema: { type: string }, description: Opaque pagination cursor (meta.next_cursor). }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }

  /api/v1/compare:
    get:
      tags: [Global]
      summary: Compare any entity across two cycles
      description: Generic form of the per-entity compare. Requires `collection` + `id` + `from` + `to`.
      parameters:
        - { name: collection, in: query, required: true, schema: { type: string, enum: [airport, aircraft, airman, charts] } }
        - { name: id, in: query, required: true, schema: { type: string } }
        - $ref: "#/components/parameters/from"
        - $ref: "#/components/parameters/to"
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }

  /api/v1/permalink:
    post:
      tags: [Permalinks]
      summary: Freeze a result or time-sequence
      description: |
        Freeze an idempotent read (or a time-sequence) into an immutable artifact replayable
        forever at `/r/{id}`. Only read endpoints can be frozen — admin/mutating targets,
        the `/r` alias and status are refused, so the gated surface can never become a
        public artifact.
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PermalinkRequest" }
      responses:
        "201": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
  /api/v1/permalink/{id}:
    get:
      tags: [Permalinks]
      summary: Replay a permalink (canonical path)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /r/{id}:
    get:
      tags: [Permalinks]
      summary: Replay a permalink (short alias)
      description: Shareable short alias for a frozen permalink; replays the exact stored envelope.
      security: []
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/NotInMirror" }

  /api/v1/wx/metar/{station}:
    get:
      tags: [Realtime]
      summary: Latest METAR for a station
      description: |
        The most recent captured METAR for one station, from the ephemeral realtime
        mirror (KV). Mirror-only — `meta.upstream.status` is always `skipped` and
        `meta.cycle` is `null` (these feeds are not cycle-versioned). Weather carries
        no PII. `meta.captured_at` is the poll timestamp.

        `data.decoded` is an additive, read-time `aerospace-derived` breakdown of the
        raw METAR (never persisted into the mirror): an ordered list of groups, each
        `{ token, label, text, kind, value }`, split into `body` (before `RMK`) and
        `remarks` (after), with `unknown` listing any token that could not be classified
        (nothing is ever dropped). It is `null` when a capture carries no raw string.
      parameters:
        - { name: station, in: path, required: true, schema: { type: string, pattern: "^[A-Za-z0-9]{3,4}$" }, example: KJFK }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }

  /api/v1/wx/taf/{station}:
    get:
      tags: [Realtime]
      summary: Latest TAF for a station
      description: |
        The most recent captured TAF (terminal aerodrome forecast) for one station,
        with lightly-decoded forecast periods. Mirror-only; no PII; not cycle-versioned.
      parameters:
        - { name: station, in: path, required: true, schema: { type: string, pattern: "^[A-Za-z0-9]{3,4}$" }, example: KLAX }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }

  /api/v1/wx/sigmet:
    get:
      tags: [Realtime]
      summary: Active SIGMET / AIRMET hazards
      description: |
        All active SIGMET/AIRMET hazard areas from the latest capture (an empty list
        before the first poll, never a 404). Each item includes the hazard polygon
        coordinates. Mirror-only; not cycle-versioned.
      responses:
        "200": { $ref: "#/components/responses/Envelope" }

  /api/v1/wx/winds:
    get:
      tags: [Realtime]
      summary: Winds & temperatures aloft near a point
      description: |
        The latest mirrored FB winds/temps-aloft bulletin (FAA/NWS FD product), read
        from the ephemeral realtime tier and reduced to the **nearest geolocated FD
        station** to `lat`/`lon`. With `fl` (flight level, hundreds of feet) the response
        carries the single nearest forecast level (`exact` flags an exact altitude match);
        without `fl` it returns every level for that station. Decoded fields per level:
        `dir_deg` (true), `speed_kt`, `temp_c` (implied-negative at/above FL240),
        `light_and_variable`. Mirror-only — `meta.upstream.status` is always `skipped`
        and the read never hits the network. A `404` (`ERROR_NOT_IN_MIRROR`) means no winds
        capture exists yet; a `501` (`ERROR_NOT_IMPLEMENTED`) is returned honestly when a
        bulletin **is** captured but its FD stations aren't geolocated yet (they're keyed by
        3-letter FD id, not coordinates), so a nearest-station lookup can't run — the message
        reports how many stations were captured and `meta.captured_at`/`meta.freshness` still
        carry the capture vintage. No PII; not AIRAC-versioned
        (`meta.cycle` is null) — captured continuously off the realtime cron. Winds
        between stations are **not** interpolated (nearest-station-at-level, v1).
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number, minimum: -90, maximum: 90 }, example: 39.86 }
        - { name: lon, in: query, required: true, schema: { type: number, minimum: -180, maximum: 180 }, example: -104.67 }
        - { name: fl, in: query, schema: { type: integer, minimum: 0 }, description: Flight level (hundreds of feet). Omit for all levels at the nearest station., example: 180 }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }
        "501": { $ref: "#/components/responses/ErrorEnvelopeResponse" }

  /api/v1/tfr:
    get:
      tags: [Realtime]
      summary: Active Temporary Flight Restrictions (headline list)
      description: |
        The active-TFR headline list from the latest capture (`id`, `notam_id`, type,
        facility, state, effective/expire, `has_geometry`). Use `/api/v1/tfr/{id}` for
        the full record and `/api/v1/tfr.geojson` for the map overlay. Mirror-only.
      responses:
        "200": { $ref: "#/components/responses/Envelope" }

  /api/v1/tfr.geojson:
    get:
      tags: [Realtime]
      summary: Active TFRs vector overlay (raw GeoJSON)
      description: |
        A raw `application/geo+json` **FeatureCollection** of active TFR polygons for
        the map overlay — **not** wrapped in the fleet envelope (MapLibre needs a bare
        FeatureCollection), exactly like `/api/v1/airports.geojson`. Provenance is in
        the `X-Source`/`X-Captured-At`/`X-Cache` headers. Mirror-only; no PII.
      responses:
        "200":
          description: A GeoJSON FeatureCollection of active TFR polygons.
          headers:
            X-Source: { schema: { type: string }, description: Provenance (faa-tfr). }
            X-Captured-At: { schema: { type: string }, description: Capture timestamp. }
            X-Cache: { schema: { type: string, enum: [hit, miss] } }
          content:
            application/geo+json:
              schema: { type: object }

  /api/v1/tfr/{id}:
    get:
      tags: [Realtime]
      summary: One TFR (full detail)
      description: |
        The full captured TFR record (effective/expire, vertical limits, purpose and
        boundary geometry) for one id. The `id` is the underscore slug of the FAA NOTAM
        id (e.g. `6/8221` → `6_8221`). Mirror-only; not cycle-versioned.
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, pattern: "^[A-Za-z0-9_-]{3,20}$" }, example: "6_8221" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }

  /api/v1/tfr/affecting/{airport}:
    get:
      tags: [Derived, Realtime]
      summary: Active TFRs affecting an airport
      description: |
        Ray-casting point-in-polygon of an airport coordinate against each active TFR
        ring from the realtime tier (reusing the stored geometry — **no upstream
        fetch**). An optional `buffer` (NM) also matches TFRs whose ring passes within
        that distance. `aerospace-derived`; mirror-only. Each result carries how it
        matched (`contains` | `within_buffer`).
      parameters:
        - { name: airport, in: path, required: true, schema: { type: string, pattern: "^[A-Za-z0-9]{2,7}$" }, example: KJFK }
        - { name: buffer, in: query, schema: { type: number }, description: Optional match buffer in NM (default 0, cap 250). }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/NotInMirror" }

  /api/v1/notams:
    get:
      tags: [Realtime]
      summary: NOTAMs for an airport (on-demand) or the national mirror index
      description: |
        Two read modes under one path.

        **On-demand per-airport (`?location=`)** — unchanged. Per-airport NOTAMs from the FAA
        NMS-API: a **fetch-through** read served from a short (~2 min) cache, otherwise fetched
        live, normalized, cached, and archived. The `data` payload is `{ airport, count, notams[] }`;
        each NOTAM carries normalized fields (`number`, `type`, `classification`, `location`,
        `icao_location`, `effective_start`, `effective_end` — the literal `"PERM"` for permanent
        NOTAMs, with `is_permanent: true` — `simple_text`/`text`, `fir`, `q_code`, `radius_nm`,
        `geometry`, …). NMS 2.5 permits returned `icao_location` values of 3–5 alphanumeric
        characters; they are preserved verbatim. `meta` carries a `freshness` block plus top-level
        `source` (`"faa-nms-api"`) and `environment`.

        **National mirror index (`?changed_since=` and/or `?classification=`)** — Phase-2 national
        forever-mirror. Served entirely from the local mirror (D1 index + R2 snapshots), never a
        live FAA call (`meta.upstream.status: "skipped"`). `?changed_since={ISO}` returns every NOTAM
        **version** updated since the given UTC time; `?classification={DOM|INTL|LMIL|FDC|…}` lists the
        **current** active set for a classification. The payload is `{ query, count, notams[] }` where
        each row is a metadata pointer (`id`, `number`, `location`, `icao_location`, `classification`,
        `type`, effective window, `last_updated`, `content_sha256`, `r2_key`) — resolve the full text
        via `/api/v1/notams/{id}`. The national mirror is populated by a gated daily bulk + half-hourly
        delta ingest (`NOTAM_NATIONAL_INGEST`); reads work against whatever is mirrored.

        Provide **exactly one** of `location`, `changed_since` or `classification`.

        **Gated by configuration.** Requires the FAA NMS-API OAuth2 client
        (`NOTAM_CLIENT_ID` / `NOTAM_CLIENT_SECRET`) and its endpoints (`NOTAM_AUTH_URL` /
        `NOTAM_API_BASE`); when unconfigured every NOTAM read returns a clean **501**
        (`ERROR_NOTAM_UNAVAILABLE`). For active airspace restrictions, see also `/api/v1/tfr`.
      parameters:
        - name: location
          in: query
          required: false
          description: Upstream NMS `Location` query — a 3–4 character ICAO/domestic ident (e.g. `KJFK` or `JFK`). This is distinct from the returned NMS 2.5 `icao_location`, which may contain 3–5 characters. Aliases — `icao`, `id` — are accepted. On-demand fetch-through mode.
          schema: { type: string, pattern: "^[A-Za-z0-9]{3,4}$" }
          example: KJFK
        - name: icao
          in: query
          required: false
          description: Alias for `location`.
          schema: { type: string, pattern: "^[A-Za-z0-9]{3,4}$" }
        - name: id
          in: query
          required: false
          description: Alias for `location`.
          schema: { type: string, pattern: "^[A-Za-z0-9]{3,4}$" }
        - name: changed_since
          in: query
          required: false
          description: National mirror — ISO-8601 UTC time; returns NOTAM versions updated since then.
          schema: { type: string, format: date-time }
          example: "2026-06-11T00:00:00Z"
        - name: classification
          in: query
          required: false
          description: National mirror — list the current active set for a classification (DOM/INTL/LMIL/FDC/…). Combine with `changed_since` to filter the version scan.
          schema: { type: string, pattern: "^[A-Za-z]{2,8}$" }
          example: DOM
        - { $ref: "#/components/parameters/limit" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "501": { $ref: "#/components/responses/ErrorEnvelopeResponse" }
        "502": { $ref: "#/components/responses/ErrorEnvelopeResponse" }

  /api/v1/notams/{id}:
    get:
      tags: [Realtime]
      summary: A single NOTAM by FAA id (national mirror)
      description: |
        The latest mirrored version of one NOTAM by its FAA id (the owner's "individual NOTAM
        pulls"). Served from the mirror only — the NMS-API has no by-id path, so this resolves
        the `notam_current` pointer + the content-addressed R2 snapshot (`meta.upstream.status:
        "skipped"`, `meta.cache.layer: "d1"`). The payload is `{ id, number, notam, version }`
        where `notam` is the full normalized record and `version` carries `{ last_updated,
        content_sha256, r2_key, ingest_cycle, active }`. **404** when the id is not (yet) in the
        mirror; **501** when NOTAM is unconfigured.
      parameters:
        - name: id
          in: path
          required: true
          description: FAA NOTAM id (e.g. `NMS_ID_1776397034092645`).
          schema: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/ErrorEnvelopeResponse" }
        "501": { $ref: "#/components/responses/ErrorEnvelopeResponse" }

  /api/v1/notams/by-number/{number}:
    get:
      tags: [Realtime]
      summary: A single NOTAM by human number (national mirror)
      description: |
        The latest mirrored version of one NOTAM by its human number (e.g. `04/278`). Same
        mirror-only semantics + payload as `/notams/{id}`. **URL-encode the slash** in the number
        (`04%2F278`). **404** when not in the mirror; **400** on a malformed number; **501** when
        NOTAM is unconfigured.
      parameters:
        - name: number
          in: path
          required: true
          description: NOTAM number with the slash URL-encoded (e.g. `04%2F278`).
          schema: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "404": { $ref: "#/components/responses/ErrorEnvelopeResponse" }
        "501": { $ref: "#/components/responses/ErrorEnvelopeResponse" }

  /api/v1/nav/nearest:
    get:
      tags: [Derived, Navigation]
      summary: Nearest navaids / fixes / airports to a point
      description: |
        Ranks mirror nav features by great-circle distance from a point, each with
        `distance_nm` and true `bearing_deg`. Mixed `type`s rank together by absolute
        distance ascending. `aerospace-derived` (computed at read time over the D1
        mirror): `meta.sources` includes `aerospace-derived` and `meta.upstream.status`
        is `skipped`.
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number, minimum: -90, maximum: 90 }, example: 40.6398 }
        - { name: lon, in: query, required: true, schema: { type: number, minimum: -180, maximum: 180 }, example: -73.7787 }
        - { name: type, in: query, schema: { type: string, enum: [navaid, fix, airport, all], default: all } }
        - { name: radius, in: query, schema: { type: number }, description: Optional max distance in NM (cap 1000). }
        - { $ref: "#/components/parameters/limit" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }

  /api/v1/nav/route:
    get:
      tags: [Derived, Navigation]
      summary: Great-circle route between waypoints
      description: |
        Resolves `from`/`to` (and an ordered, comma- or whitespace-delimited `via` list)
        to mirror coordinates — airport ICAO/LID → navaid ident → fix name, authority-
        agnostic — and returns ordered legs (`distance_nm`, initial/final true bearing,
        cumulative) plus the total. `aerospace-derived`; mirror-only. Airway-designator
        expansion is deferred (`via` tokens are explicit waypoints).
      parameters:
        - { name: from, in: query, required: true, schema: { type: string }, example: KJFK }
        - { name: to, in: query, required: true, schema: { type: string }, example: KLAX }
        - { name: via, in: query, schema: { type: string }, description: Ordered intermediate idents, comma- or space-delimited., example: "CEVIC,DOVER" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }

  /api/v1/nav/along-route:
    get:
      tags: [Derived, Navigation]
      summary: Features along a route corridor
      description: |
        Navaids/fixes within `corridor` NM cross-track of the great-circle path, plus
        active TFRs whose ring contains (or lies within `corridor` NM of) a sampled point
        on the path. `aerospace-derived`; reads the D1 nav mirror + the realtime KV TFR
        tier only. Airspace, obstacle and live-NOTAM **corridor enrichment** is still
        **deferred** (reported in `data.deferred` / `meta.deferred`): the FAA DOF obstacle
        dataset is now mirrored — query it directly via `/api/v1/obstacles.geojson` or
        `/api/v1/obstacle/{id}` — but cross-track obstacle filtering along the route is not
        wired into this endpoint yet.
      parameters:
        - { name: from, in: query, required: true, schema: { type: string }, example: KJFK }
        - { name: to, in: query, required: true, schema: { type: string }, example: KLAX }
        - { name: via, in: query, schema: { type: string }, description: Ordered intermediate idents, comma- or space-delimited. }
        - { name: corridor, in: query, schema: { type: number, default: 10 }, description: Cross-track half-width in NM (cap 100). }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }

  /api/v1/plan:
    post:
      tags: [Derived, Navigation]
      summary: Advisory flight plan (route + winds aloft + performance)
      description: |
        **ADVISORY / NON-CERTIFIED.** Builds a coarse planning estimate by reusing the
        Part A great-circle engine for the route (`from`/`to`/`via`, resolved exactly like
        `/api/v1/nav/route`), overlaying the latest mirrored winds-aloft capture at the
        cruise level near each leg's midpoint, and applying a textbook wind triangle
        against a small static per-ICAO-type performance seed (`C172`, `PA28`, `BE36`,
        `B738`, `A320`) or an explicit `performance` override. Returns per-leg wind
        components (head/cross), wind-correction angle, ground speed, ETE and fuel, plus
        cumulative totals and — when no cruise altitude is given — a suggested wind-favored
        FD level. `aerospace-derived`; mirror-only (`meta.upstream.status` = `skipped`),
        permalinkable. These numbers come from a static seed and a spherical-earth wind
        triangle: they are **NOT** for operational flight planning, dispatch, or fuel-load
        decisions. Deferred (see `data.deferred`): GFS gridded winds, inter-station winds
        interpolation, climb/descent/taxi/reserve fuel modelling, and airspace/obstacle/
        NOTAM enrichment (use `/api/v1/nav/along-route`; the DOF obstacle dataset itself is
        now mirrored at `/api/v1/obstacle/{id}` + `/api/v1/obstacles.geojson`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [from, to]
              properties:
                from: { type: string, description: Departure ident (airport ICAO/LID, navaid ident or fix name)., example: KJFK }
                to: { type: string, description: Destination ident., example: KBOS }
                via: { description: Ordered intermediate idents (array, or comma/space-delimited string)., oneOf: [ { type: string }, { type: array, items: { type: string } } ], example: "HFD" }
                aircraft_type: { type: string, description: "Seeded ICAO type: C172, PA28, BE36, B738 or A320.", example: C172 }
                performance:
                  type: object
                  description: Explicit performance override (wins over the seed table).
                  required: [tas_kt, burn_per_hr]
                  properties:
                    tas_kt: { type: number, exclusiveMinimum: 0, description: True airspeed (kt). }
                    burn_per_hr: { type: number, minimum: 0, description: Fuel burn per hour. }
                    fuel_unit: { type: string, description: Free-text fuel unit label (e.g. gal_avgas, lb_jeta). }
                cruise_fl: { type: integer, description: Requested cruise flight level (hundreds of feet). Omit to let the planner suggest one., example: 80 }
                altitude: { type: integer, description: Requested cruise altitude (feet MSL); alternative to cruise_fl. }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "405": { $ref: "#/components/responses/ErrorEnvelopeResponse" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
    get:
      tags: [Derived, Navigation]
      summary: Advisory flight plan (shareable/permalinkable query form)
      description: |
        The GET form of `POST /api/v1/plan` — identical advisory planner, with the body
        flattened into query parameters so a plan can be shared as a link or frozen into a
        permalink (a frozen plan re-runs as a GET). The `performance` override is expressed
        as `tas_kt`/`burn_per_hr`/`fuel_unit` query params. See the POST description for
        the full advisory framing and deferrals.
      parameters:
        - { name: from, in: query, required: true, schema: { type: string }, example: KJFK }
        - { name: to, in: query, required: true, schema: { type: string }, example: KBOS }
        - { name: via, in: query, schema: { type: string }, description: Ordered intermediate idents, comma- or space-delimited. }
        - { name: aircraft_type, in: query, schema: { type: string }, description: "Seeded ICAO type (C172, PA28, BE36, B738, A320).", example: C172 }
        - { name: cruise_fl, in: query, schema: { type: integer }, description: Cruise flight level (hundreds of feet). Omit for a suggestion. }
        - { name: altitude, in: query, schema: { type: integer }, description: Cruise altitude (feet MSL); alternative to cruise_fl. }
        - { name: tas_kt, in: query, schema: { type: number }, description: Performance override — true airspeed (kt). }
        - { name: burn_per_hr, in: query, schema: { type: number }, description: Performance override — fuel burn per hour. }
        - { name: fuel_unit, in: query, schema: { type: string }, description: Performance override — fuel unit label. }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }

  /api/v1/magvar:
    get:
      tags: [Derived]
      summary: Magnetic variation (declination) — NOAA WMM2025
      description: |
        Magnetic declination (magvar), inclination and field intensity at a point from
        the embedded NOAA **World Magnetic Model 2025** (degree/order-12 Gauss
        coefficients, public domain, committed in-repo). Pure `aerospace-derived`
        compute: NO upstream fetch and nothing persisted (`meta.upstream.status` =
        `skipped`, `meta.cache.status` = `bypass`, `meta.sources` = `[wmm]`).
        Complements the TRUE-degree runway headings (`le_heading`/`he_heading`) and
        navaid radials already in the mirror — true ⇄ magnetic anywhere on Earth. Valid
        `2025.0`–`2030.0`; a `date` outside that window is clamped to the nearest bound
        and flagged `out_of_range` (still computed). Only a missing/invalid
        `lat`/`lon`/`alt`/`date` is a 400.
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number, minimum: -90, maximum: 90 }, example: 40.64, description: Geodetic latitude (degrees). }
        - { name: lon, in: query, required: true, schema: { type: number, minimum: -180, maximum: 180 }, example: -73.78, description: Longitude (degrees). }
        - { name: alt, in: query, schema: { type: number, default: 0 }, example: 0, description: Altitude above the WGS84 ellipsoid (km). }
        - { name: date, in: query, schema: { type: string }, example: 2026-06-19, description: "ISO-8601 date/time or a decimal year (e.g. 2026.5). Default: today (UTC)." }
      responses:
        "200":
          description: WMM2025 magnetic field at the requested point.
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: "#/components/schemas/Envelope" }
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/MagVar" }
        "400": { $ref: "#/components/responses/ValidationError" }

  /api/v1/daylight:
    get:
      tags: [Derived]
      summary: Sunrise / sunset, twilights & FAA night — solar position (computed)
      description: |
        Sunrise, sunset, the three twilights (civil/nautical/astronomical), solar noon
        and day length for a point and date, computed from the embedded **NOAA Solar
        Calculator / Meeus** sunrise-equation algorithm (public domain, implemented
        in-repo). Pure `aerospace-derived` compute: NO upstream fetch and nothing
        persisted (`meta.upstream.status` = `skipped`, `meta.cache.status` = `bypass`,
        `meta.sources` = `[solar]`). All event times are ISO-8601 **UTC**, `null` where
        the sun never reaches that altitude (polar day/night — see `polar`).

        Aviation relevance: the FAA defines **night** (14 CFR 1.1) as the period from the
        END of evening civil twilight to the BEGINNING of morning civil twilight — exposed
        explicitly as `faa_night_begin` / `faa_night_end` — which governs position lights
        and night currency/logging. Altitudes used: sunrise/sunset −0.833° (refraction +
        semidiameter), civil −6°, nautical −12°, astronomical −18°. Validated against US
        Naval Observatory tables (±2 minutes). Only a missing/out-of-range `lat`/`lon` or a
        malformed `date` is a 400.
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number, minimum: -90, maximum: 90 }, example: 40.64, description: Geodetic latitude (degrees). }
        - { name: lon, in: query, required: true, schema: { type: number, minimum: -180, maximum: 180 }, example: -73.78, description: Longitude (degrees, East-positive). }
        - { name: date, in: query, schema: { type: string }, example: 2026-06-21, description: "ISO-8601 date or datetime; only the UTC calendar date is used. Default: today (UTC)." }
      responses:
        "200":
          description: Solar events at the requested point for the date (UTC).
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: "#/components/schemas/Envelope" }
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Daylight" }
        "400": { $ref: "#/components/responses/ValidationError" }

  /api/v1/atmosphere:
    get:
      tags: [Derived]
      summary: US Standard Atmosphere 1976 — temperature, pressure, density, speed of sound
      description: |
        Temperature, pressure, density and speed of sound at any altitude from 0 to 86 km
        (282,152 ft), computed from the embedded **US Standard Atmosphere 1976** (ISO 2533)
        7-layer piecewise model. Pure `aerospace-derived` compute: NO upstream fetch and
        nothing persisted (`meta.upstream.status` = `skipped`, `meta.cache.status` =
        `bypass`, `meta.sources` = `[isa]`). Optionally computes **pressure altitude** (PA)
        given an altimeter setting (QNH) and **density altitude** (DA) given OAT.
      parameters:
        - { name: altitude, in: query, required: true, schema: { type: number }, example: 11000, description: "Altitude value in the selected unit (default: metres)." }
        - { name: unit, in: query, schema: { type: string, enum: [m, ft], default: m }, example: ft, description: "Altitude unit: 'm' (metres, default) or 'ft' (feet)." }
        - { name: altimeter, in: query, schema: { type: number }, example: 1013.25, description: "Altimeter setting / QNH in hPa. When provided, computes pressure altitude." }
        - { name: field_elev, in: query, schema: { type: number }, description: "Field elevation in the selected unit. Used with altimeter for PA." }
        - { name: oat, in: query, schema: { type: number }, description: "Outside air temperature in °C. When provided with altimeter, computes density altitude." }
      responses:
        "200":
          description: ISA properties at the requested altitude.
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: "#/components/schemas/Envelope" }
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Atmosphere" }
        "400": { $ref: "#/components/responses/ValidationError" }

  /api/v1/admin/ingest/nasr:
    post:
      tags: [Admin]
      summary: Ingest a NASR cycle (airports/runways)
      security: [{ adminKey: [] }]
      requestBody: { $ref: "#/components/requestBodies/Ingest" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "405": { $ref: "#/components/responses/Envelope" }
        "503": { $ref: "#/components/responses/Envelope" }
  /api/v1/admin/ingest/aircraft:
    post:
      tags: [Admin]
      summary: Ingest an aircraft registry snapshot (PII)
      security: [{ adminKey: [] }]
      requestBody: { $ref: "#/components/requestBodies/Ingest" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "503": { $ref: "#/components/responses/Envelope" }
  /api/v1/admin/ingest/airmen:
    post:
      tags: [Admin]
      summary: Ingest an airmen certification snapshot (PII)
      security: [{ adminKey: [] }]
      requestBody: { $ref: "#/components/requestBodies/Ingest" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "503": { $ref: "#/components/responses/Envelope" }
  /api/v1/admin/ingest/charts:
    post:
      tags: [Admin]
      summary: Ingest a chart catalog cycle
      security: [{ adminKey: [] }]
      requestBody: { $ref: "#/components/requestBodies/Ingest" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "503": { $ref: "#/components/responses/Envelope" }
  /api/v1/admin/ingest/dof:
    post:
      tags: [Admin]
      summary: Ingest an FAA Digital Obstacle File cycle
      description: |
        Load a DOF obstacle cycle from caller-provided raw text — `{ "dat": "<DOF.Dat>" }`
        for a single file, or `{ "dat": { "DOF.Dat": "…", "53-CA.Dat": "…" } }` to archive
        several per-state members under their own names. Ingest is inline, to stay within
        the Worker memory limit like the other large datasets. Raw bytes are archived
        verbatim to R2 before parsing. No PII.
      security: [{ adminKey: [] }]
      requestBody: { $ref: "#/components/requestBodies/Ingest" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "502": { $ref: "#/components/responses/ErrorEnvelopeResponse" }
        "503": { $ref: "#/components/responses/Envelope" }
  /api/v1/admin/ingest/dtpp:
    post:
      tags: [Admin]
      summary: Ingest an FAA d-TPP (terminal procedures) cycle
      description: |
        Two bounded, idempotent phases keep a full US cycle (~3,197 airports) under the
        Workers 10,000-subrequest-per-invocation limit.

        **`phase=index`** (default) — load the cycle index from the ~15 MB
        `d-TPP_Metafile.xml`. `{ "live": true }` downloads the current cycle's metafile from
        FAA; `{ "xml": "<digital_tpp …>" }` supplies it inline (tests/ops); `cycle` (YYMM)
        pins which cycle to fetch. The raw metafile is archived to R2 write-once **before**
        parsing, then the thin `terminal_procedures` index is upserted and the cycle booked
        (~483 subrequests). This alone lights up `/airport/{id}/charts` and the PDF serve
        route, which read only the index. PDFs are **not** bulk-downloaded here (mirrored on
        demand by the serve route).

        **`phase=entities`** — bounded, **resumable** per-airport entity/history backfill,
        read back from the already-populated index a page at a time (it does **not** re-
        download, re-parse, or re-archive). Page with `offset`/`limit` (default 1,000, max
        1,200 airports) and repeat with the returned `next_offset` until `data.done` is true;
        `cycle` defaults to the latest cycle in the index.

        Gated by the `DTPP_ENABLED` flag in addition to `X-Admin-Key`. No PII.
      security: [{ adminKey: [] }]
      parameters:
        - { in: query, name: phase, required: false, schema: { type: string, enum: [index, entities], default: index }, description: Ingest phase. }
        - { in: query, name: cycle, required: false, schema: { type: string, pattern: "^[0-9]{4}$" }, description: "AIRAC cycle (YYMM); defaults to current (index) / latest-in-index (entities)." }
        - { in: query, name: offset, required: false, schema: { type: integer, minimum: 0 }, description: "phase=entities: airport page offset." }
        - { in: query, name: limit, required: false, schema: { type: integer, minimum: 1, maximum: 1200, default: 1000 }, description: "phase=entities: airports per page." }
      requestBody: { $ref: "#/components/requestBodies/Ingest" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Envelope" }
        "502": { $ref: "#/components/responses/ErrorEnvelopeResponse" }
        "503": { $ref: "#/components/responses/Envelope" }
  /api/v1/admin/ingest/cifp:
    post:
      tags: [Admin]
      summary: Ingest an FAA CIFP (coded instrument procedures) cycle
      description: |
        Two bounded, idempotent phases keep a full US cycle (~14,300 procedures across
        ~3,072 airports) under the Workers 10,000-subrequest-per-invocation limit.

        **`phase=index`** (default) — download the ~9 MB `CIFP_YYMMDD.zip`, archive the
        raw zip **and** the extracted `FAACIFP18` datafile to R2 write-once **before**
        parsing, then parse the ARINC 424 SID/STAR/approach leg records, upsert the thin
        `procedures` index and book the cycle. `{ "live": true }` downloads the current
        cycle from FAA; `{ "zip": "<base64>" }` or `{ "text": "<FAACIFP18>" }` supplies it
        inline (tests/ops); `cycle` (YYMMDD effective date) pins which cycle to fetch. This
        lights up `/airport/{id}/procedures` and the per-procedure read routes.

        **`phase=entities`** — bounded, **resumable** per-procedure entity/history
        backfill. Page with `offset`/`limit` (default 1,500, max 1,800 procedures) and
        repeat with the returned `next_offset` until `data.done` is true; `cycle` defaults
        to the latest cycle in the index.

        Gated by the `CIFP_ENABLED` flag in addition to `X-Admin-Key`. No PII.
      security: [{ adminKey: [] }]
      parameters:
        - { in: query, name: phase, required: false, schema: { type: string, enum: [index, entities], default: index }, description: Ingest phase. }
        - { in: query, name: cycle, required: false, schema: { type: string, pattern: "^[0-9]{6}$" }, description: "AIRAC cycle (YYMMDD effective date); defaults to current (index) / latest-in-index (entities)." }
        - { in: query, name: offset, required: false, schema: { type: integer, minimum: 0 }, description: "phase=entities: procedure page offset." }
        - { in: query, name: limit, required: false, schema: { type: integer, minimum: 1, maximum: 1800, default: 1500 }, description: "phase=entities: procedures per page." }
      requestBody: { $ref: "#/components/requestBodies/Ingest" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Envelope" }
        "502": { $ref: "#/components/responses/ErrorEnvelopeResponse" }
        "503": { $ref: "#/components/responses/Envelope" }
  /api/v1/admin/ingest/ourairports:
    post:
      tags: [Admin]
      summary: Ingest an OurAirports global snapshot
      description: |
        Load an OurAirports nightly snapshot into the mirror under `authority=ourairports`
        (additive to FAA data). Inline `csv` (`airports` required; `runways`, `navaids`,
        `frequencies`, `countries`, `regions` optional) seeds from provided CSV text; otherwise
        the nightly files are downloaded. Either way each raw file is archived to R2 write-once
        before parsing. `cycle` defaults to today (UTC) — the snapshot date, not an AIRAC cycle.
      security: [{ adminKey: [] }]
      requestBody: { $ref: "#/components/requestBodies/Ingest" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "405": { $ref: "#/components/responses/Envelope" }
        "502": { $ref: "#/components/responses/Envelope" }
        "503": { $ref: "#/components/responses/Envelope" }

  /api/v1/admin/ingest/openflights:
    post:
      tags: [Admin]
      summary: Ingest an OpenFlights snapshot (airlines / routes / planes)
      description: |
        Load an OpenFlights snapshot into the mirror under `authority=openflights` (additive;
        lands in the shared `aero-core` shard). Inline `dat` (`airlines` required; `routes`,
        `planes` optional) seeds from provided `.dat` text; otherwise the current files are
        downloaded from the OpenFlights GitHub repo (`?ref=` pins a commit if `master` 404s).
        Either way each raw file is archived to R2 write-once before parsing. `cycle` defaults
        to today (UTC) — the snapshot date, not an AIRAC cycle. ODbL — attribute "OpenFlights".
      security: [{ adminKey: [] }]
      requestBody: { $ref: "#/components/requestBodies/Ingest" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "405": { $ref: "#/components/responses/Envelope" }
        "502": { $ref: "#/components/responses/Envelope" }
        "503": { $ref: "#/components/responses/Envelope" }

  /api/v1/admin/poll/wx:
    post:
      tags: [Admin]
      summary: Run a weather poll now (METAR/TAF/SIGMET)
      description: |
        Trigger a weather capture immediately instead of waiting for the 5-minute cron.
        Optional `?stations=KJFK,KLAX` overrides the polled station set. The capture is
        written to the ephemeral realtime tier (KV latest + the REALTIME R2 bucket).
      security: [{ adminKey: [] }]
      parameters:
        - { name: stations, in: query, schema: { type: string }, description: Comma-separated station override. }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "502": { $ref: "#/components/responses/ErrorEnvelopeResponse" }

  /api/v1/admin/poll/tfr:
    post:
      tags: [Admin]
      summary: Run a TFR poll now
      description: |
        Trigger a TFR capture immediately. Optional `?detail_limit=N` caps how many
        TFRs are enriched with their detail document (effective/expire/geometry).
      security: [{ adminKey: [] }]
      parameters:
        - { name: detail_limit, in: query, schema: { type: integer }, description: Max per-TFR detail fetches. }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "502": { $ref: "#/components/responses/ErrorEnvelopeResponse" }

  /api/v1/admin/poll/winds:
    post:
      tags: [Admin]
      summary: Run a winds-aloft poll now
      description: |
        Trigger a winds/temps-aloft (FD bulletin) capture immediately instead of waiting
        for the throttled realtime cron (winds are only reissued a few times a day, so the
        scheduled poll skips when the latest capture is recent). Optional `?region=`,
        `?fcst=` (06/12/24) and `?level=` (low/high) select the FD product. The capture is
        written to the ephemeral realtime tier (KV latest + the REALTIME R2 bucket); the
        ARCHIVE bucket is never touched.
      security: [{ adminKey: [] }]
      parameters:
        - { name: region, in: query, schema: { type: string }, description: FD region (e.g. all, bos, chi, dfw, sfo). }
        - { name: fcst, in: query, schema: { type: string, enum: ["06", "12", "24"] }, description: Forecast period (hours). }
        - { name: level, in: query, schema: { type: string, enum: [low, high] }, description: Altitude band. }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "502": { $ref: "#/components/responses/ErrorEnvelopeResponse" }

  /api/v1/admin/prune/realtime:
    post:
      tags: [Admin]
      summary: Prune ephemeral realtime captures now
      description: |
        Force the 90-day ephemeral prune sweep over the REALTIME bucket (the cron runs
        it at most once/day). Optional `?max_age_days=N`. This NEVER touches the
        permanent ARCHIVE bucket — the ephemeral tier is a physically separate bucket.
      security: [{ adminKey: [] }]
      parameters:
        - { name: max_age_days, in: query, schema: { type: integer, default: 90 } }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "502": { $ref: "#/components/responses/ErrorEnvelopeResponse" }

  /api/v1/admin/ingest/notam-national:
    post:
      tags: [Admin]
      summary: Ingest the national NOTAM initial-load now (gated)
      description: |
        Trigger the daily national NOTAM **initial-load** (the full active set, AIXM-in-SOAP,
        gzip) — the cron's manual counterpart, for validation/backfill. The raw file is streamed
        write-once into the permanent ARCHIVE bucket
        (`notam/national/il/{YYYY-MM-DD}/national.aixm.soap.gz` + a sibling manifest) and stored
        **forever**; byte-identical days are stored once (content-addressed dedup), and the
        archive is never deleted. **Gated by `NOTAM_NATIONAL_INGEST`** — a clean
        `{status:"skipped"}` no-op (zero FAA calls) when disabled. Default `?mode=queue` enqueues
        the job onto BULK_QUEUE (the production path); `?mode=inline` runs it synchronously.
        Optional `?classification=DOM|INTL|LMIL|FDC|…` fetches a smaller per-classification shard.
        Every scheduled NMS-API pull passes a shared **pace-gate** (min ~3 min between upstream
        pulls, plus a post-429 cooldown) honoring the FAA 1-pull/3-min credential cap; when the
        gate is closed this returns `{status:"deferred", reason:"faa-pace-gate"|"faa-cooldown",
        retry_after_s}` with **zero FAA calls** (a queued job is retried with delay so the
        once-daily load just slides past the gate — never dropped). A same-day re-run finds the
        day already archived and returns `{status:"exists"}` **before** any token/content fetch.
      security: [{ adminKey: [] }]
      parameters:
        - { name: mode, in: query, schema: { type: string, enum: [queue, inline], default: queue } }
        - { name: classification, in: query, schema: { type: string, pattern: "^[A-Za-z]{2,8}$" }, description: Optional per-classification shard. }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "502": { $ref: "#/components/responses/ErrorEnvelopeResponse" }

  /api/v1/admin/poll/notam-delta:
    post:
      tags: [Admin]
      summary: Run the NOTAM changed-since delta now (gated)
      description: |
        Pull the FAA NMS-API **changed-since** GEOJSON delta and fold each NOTAM version into the
        national mirror: content-address each distinct version to R2 (`notam/version/{sha}.json`,
        forever) and idempotently upsert the NOTAMS index (`notam_versions` keyed
        `(id, last_updated)` + the `notam_current` pointer). The half-hourly cron's manual
        counterpart. **Gated by `NOTAM_NATIONAL_INGEST`** (a no-op when disabled). Optional
        `?since={ISO}` overrides the window (default: a ~23 h overlapping backfill, kept just under the FAA 24 h limit).
        Shares the NMS-API **pace-gate** with the national load: when a pull happened too recently
        (or a 429 cooldown is active) this returns `{status:"deferred", reason:"faa-pace-gate"|"faa-cooldown",
        retry_after_s}` with **zero FAA calls** — the overlapping next window covers the gap, so no
        NOTAM change is lost.
      security: [{ adminKey: [] }]
      parameters:
        - { name: since, in: query, schema: { type: string, format: date-time }, description: ISO-8601 UTC window start (absolute). }
        - { name: minutes, in: query, schema: { type: integer }, description: Relative window — look back this many minutes (ignored if `since` is set). }
        - { name: limit, in: query, schema: { type: integer }, description: Cap how many (freshest-first) NOTAMs this run processes. }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "502": { $ref: "#/components/responses/ErrorEnvelopeResponse" }

  /api/v1/admin/mci:
    get:
      tags: [Admin]
      summary: List CCA MCI links (audit)
      description: |
        Audit listing of the FAA-record → MCI links. Filters: `?type=airman|operator`,
        `?id={entity id}`, `?mci={mci}`, `?include_inactive=1` (unlinked rows are kept
        for audit, never deleted), `?limit=` (default 200, max 1000).
        Fleet workers calling over the `AEROSPACE` service binding pass keylessly;
        edge callers need `X-Admin-Key`.
      security: [{ adminKey: [] }]
      parameters:
        - { name: type, in: query, schema: { type: string, enum: [airman, operator] } }
        - { name: id, in: query, schema: { type: string } }
        - { name: mci, in: query, schema: { type: string } }
        - { name: include_inactive, in: query, schema: { type: boolean, default: false } }
        - { $ref: "#/components/parameters/limit" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }
    post:
      tags: [Admin]
      summary: Link an airman or operator to a CCA MCI
      description: |
        Create/replace the CCA **Matrix Consumer Identifier** link for one FAA record.
        `airman` links key by FAA UNIQUE ID and require a person MCI (`c…` participant /
        `x…` non-participant); `operator` links key by aircraft registration mark (the
        registry has no standalone operator id) and accept a person or entity (`e…`) MCI.
        MCIs are minted by cca-admin — this endpoint only stores the link, in aero-core
        `mci_links` (CCA-local, merged at read time; the mirror stays pure and an ingest
        can never clobber a link). Re-POSTing the same record replaces its MCI and
        reactivates an unlinked row. The response reports `in_mirror` (soft check — a
        link may precede its record's first ingest). Fleet workers calling over the
        `AEROSPACE` service binding pass keylessly; edge callers need `X-Admin-Key`.
      security: [{ adminKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [entity_type, entity_id, mci]
              properties:
                entity_type: { type: string, enum: [airman, operator] }
                entity_id: { type: string, description: FAA UNIQUE ID (airman) or registration mark (operator)., example: A0000014 }
                mci: { type: string, example: cgxc-2d82864bfda4 }
                note: { type: string }
                author: { type: string }
      responses:
        "200":
          description: Link stored.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Envelope"
                  - properties:
                      data:
                        type: object
                        properties:
                          link: { $ref: "#/components/schemas/MciLink" }
                          in_mirror: { type: boolean }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "503": { $ref: "#/components/responses/StorageUnavailable" }

  /api/v1/admin/mci/unlink:
    post:
      tags: [Admin]
      summary: Unlink an airman or operator from its MCI
      description: |
        Deactivate the MCI link for one FAA record (`{entity_type, entity_id}`). The row
        is kept inactive for audit (`GET /admin/mci?include_inactive=1`), never deleted;
        re-linking the same record reactivates it. 404 when no active link exists.
        Fleet workers calling over the `AEROSPACE` service binding pass keylessly;
        edge callers need `X-Admin-Key`.
      security: [{ adminKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [entity_type, entity_id]
              properties:
                entity_type: { type: string, enum: [airman, operator] }
                entity_id: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/admin/airman/watch:
    get:
      tags: [Admin]
      summary: List airman license watches
      description: |
        Watched airmen are re-checked against the FAA Airmen Inquiry **daily** (cron
        08:17 UTC, capped batch, oldest-scanned first). FAA sessions are paced
        (20 s apart by default, `AIRMAN_WATCH_PACE_MS`); a failed attempt gets one
        retry after a 60 s backoff (`AIRMAN_WATCH_RETRY_BACKOFF_MS`). A persistent
        failure circuit-breaks the batch, and failed watches become eligible after
        1 h for the half-hourly recovery cron. Certificate/rating changes are diffed into
        `change_log` and surface in `/changes` within a day — an unchanged rescan logs
        nothing (same-cycle re-persists are deduped). Watches enroll automatically on
        live capture (manual search) and on MCI airman linking. The
        response reports `scan_enabled` (live fallback on + `AIRMAN_WATCH_ENABLED` ≠ 0).
        Each watch also reports `last_success_at`, `last_scan_detail`, `last_error`,
        `last_error_at`, and `consecutive_error_attempts`; the last error is retained
        after recovery for incident review, while `last_scan_status` identifies the
        current attempt state.
      security: [{ adminKey: [] }]
      parameters:
        - { name: include_inactive, in: query, schema: { type: boolean }, description: Include unwatched (audit) rows. }
        - { name: limit, in: query, schema: { type: integer, maximum: 2000 } }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Admin]
      summary: Watch an airman
      description: Subscribe one airman (`{airman_id, note?}`) to the daily license scan. Idempotent — re-watching reactivates.
      security: [{ adminKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [airman_id]
              properties:
                airman_id: { type: string, pattern: "^[A-Za-z][0-9]{7}$" }
                note: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/ValidationError" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/admin/airman/watch/remove:
    post:
      tags: [Admin]
      summary: Stop watching an airman
      description: Deactivate a watch (`{airman_id}`). The row is kept for audit; re-watching reactivates it.
      security: [{ adminKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [airman_id]
              properties:
                airman_id: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotInMirror" }
  /api/v1/admin/airman/watch/scan:
    post:
      tags: [Admin]
      summary: Run a watch scan batch now
      description: |
        Manually trigger the daily license scan (`?limit=` caps the batch, default 40,
        max 200). Forces past the enabled gate and retries **errored** watches
        immediately, but healthy watches scanned within the last ~20 h are skipped, so
        a batch re-run is safe/idempotent. Set `?airman_id=A1234567` for a target-only
        recovery scan; a target bypasses the rescan window but must already have an
        active watch. FAA sessions retain the production pace/backoff/circuit breaker;
        unchanged rescans write no change-log rows. The response's `errors` and
        `issues` arrays identify stale outcomes, and `status` is `degraded` whenever
        any remain.
      security: [{ adminKey: [] }]
      parameters:
        - { name: limit, in: query, schema: { type: integer, maximum: 200 } }
        - { name: airman_id, in: query, schema: { type: string, pattern: "^[A-Za-z][0-9]{7}$" }, description: Run only this active watch and bypass its rescan window. }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /api/v1/admin/airman/watch/backfill:
    post:
      tags: [Admin]
      summary: Backfill watch enrollment
      description: One-shot enrollment of every already-captured live airman and every active MCI-linked airman that predates the watch table.
      security: [{ adminKey: [] }]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }

components:
  securitySchemes:
    wafAuth:
      type: apiKey
      in: header
      name: X-Auth
      description: Zone WAF gate for HTTP callers. Fleet workers use Cloudflare service bindings instead.
    adminKey:
      type: apiKey
      in: header
      name: X-Admin-Key
      description: Gates the admin ingest (mutation) surface.

  parameters:
    airportId:
      name: id
      in: path
      required: true
      schema: { type: string }
      example: KJFK
      description: |
        Airport identifier or alias — ICAO (`EGLL`), IATA (`LHR`), national local id
        (`00A`), or an upstream source ident, which may contain a hyphen
        (`{ISO2}-{number}`, for example `DE-0411`, up to 12 characters).
    aircraftMark:
      name: mark
      in: path
      required: true
      schema: { type: string }
      example: N12345
    airmanId:
      name: id
      in: path
      required: true
      schema: { type: string, pattern: "^[A-Z][0-9]{7}$" }
      example: A0000014
    obstacleId:
      name: id
      in: path
      required: true
      schema: { type: string, pattern: "^[A-Za-z0-9]{2}-[0-9]{6}$" }
      example: "11-024069"
      description: Canonical DOF obstacle id — 2-char OAS code + 6-digit obstacle number.
    chartProduct:
      name: product
      in: path
      required: true
      schema: { type: string }
      example: sectional-new-york
    navaidIdent:
      name: ident
      in: path
      required: true
      schema: { type: string }
      example: JFK
      description: Navaid identifier (ICAO ident where assigned).
    fixName:
      name: name
      in: path
      required: true
      schema: { type: string }
      example: CEVIC
      description: Fix / waypoint name (FAA 5-letter name codes are nationally unique).
    airwayDesignator:
      name: designator
      in: path
      required: true
      schema: { type: string }
      example: V25
      description: Airway designator (Victor / Jet / RNAV route id).
    procedureAirport:
      name: airport
      in: path
      required: true
      schema: { type: string }
      example: KJFK
      description: Airport ident (ICAO or FAA LID) the procedure belongs to.
    procedureType:
      name: type
      in: path
      required: true
      schema: { type: string, enum: [SID, STAR, IAP] }
      example: SID
      description: Procedure family — SID (departure), STAR (arrival) or IAP (approach).
    procedureIdent:
      name: ident
      in: path
      required: true
      schema: { type: string, pattern: "^[A-Z0-9][A-Z0-9-]{0,5}$" }
      example: DEEZZ6
      description: Procedure ident (ARINC 424 SID/STAR/approach name), may contain a hyphen (`RNV-A`).
    cyclePath:
      name: cycle
      in: path
      required: true
      schema: { type: string }
      example: "2026-06-11"
      description: Cycle effective date (AIRAC 28/56-day, daily or monthly per dataset) or `latest`.
    asOf:
      name: as_of
      in: query
      schema: { type: string }
      example: "2026-05-14"
      description: Time-travel to the state at/just before this cycle or ISO date.
    fields:
      name: fields
      in: query
      schema: { type: string }
      description: Comma-separated projection of top-level fields.
    refresh:
      name: refresh
      in: query
      schema: { type: boolean }
      description: Bypass the KV hot cache (re-read D1).
    limit:
      name: limit
      in: query
      schema: { type: integer, default: 25 }
    historyFormat:
      name: format
      in: query
      schema: { type: string, enum: [sequence] }
      description: Set `sequence` for the time-sequence (cycle points) shape.
    seqFrom:
      name: from
      in: query
      schema: { type: string }
      description: Sequence start cycle/date.
    seqTo:
      name: to
      in: query
      schema: { type: string }
      description: Sequence end cycle/date.
    since:
      name: since
      in: query
      schema: { type: string }
      description: Lower cycle/date bound (inclusive).
    until:
      name: until
      in: query
      schema: { type: string }
      description: Upper cycle/date bound (inclusive).
    from:
      name: from
      in: query
      required: true
      schema: { type: string }
      description: Cycle/date to compare from.
    to:
      name: to
      in: query
      required: true
      schema: { type: string }
      description: Cycle/date to compare to.

  requestBodies:
    Ingest:
      required: true
      description: Cycle + source payload (inline CSV/ZIP or a fetch descriptor). Gated by X-Admin-Key.
      content:
        application/json:
          schema:
            type: object
            properties:
              cycle: { type: string, example: "2026-06-11" }
              csv: { type: object, additionalProperties: true }
            additionalProperties: true

  responses:
    Envelope:
      description: Fleet envelope.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Envelope" }
    ValidationError:
      description: Malformed request.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    NotInMirror:
      description: The entity/cycle is not in the mirror (no upstream is ever called).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    StorageUnavailable:
      description: A required storage binding is unavailable.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    Unauthorized:
      description: Missing or wrong X-Admin-Key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
    ErrorEnvelopeResponse:
      description: An error in the fleet envelope (e.g. credential-gated 501, or an upstream poll failure).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }

  schemas:
    Envelope:
      type: object
      required: [request_status, message, data, meta]
      properties:
        request_status: { type: string, enum: [success], example: success }
        message: { type: string }
        data: {}
        meta: { $ref: "#/components/schemas/Meta" }
    ErrorEnvelope:
      type: object
      required: [request_status, error_code, message]
      properties:
        request_status: { type: string, enum: [error], example: error }
        error_code: { type: string, example: ERROR_NOT_IN_MIRROR }
        message: { type: string }
        data: { type: "null" }
        meta: { $ref: "#/components/schemas/Meta" }
    Meta:
      type: object
      description: Always present. `sources`, `cache` and `upstream` are always set; `cycle`/`as_of` appear on cycle-versioned data; `freshness` answers "as of when?" on reads that carry a datable vintage (realtime captures + the airport bundle); `count`/`limit` on lists; `permalink`/`snapshot` on replays.
      properties:
        sources: { type: array, items: { type: string }, example: [faa-nasr] }
        cache: { $ref: "#/components/schemas/Cache" }
        upstream: { $ref: "#/components/schemas/Upstream" }
        cycle:
          oneOf:
            - { $ref: "#/components/schemas/Cycle" }
            - { type: "null" }
        as_of: { type: [string, "null"] }
        freshness:
          oneOf:
            - { $ref: "#/components/schemas/Freshness" }
            - { type: "null" }
        captured_at: { type: [string, "null"], description: Realtime poll timestamp (when this worker captured the upstream feed). }
        count: { type: integer }
        limit: { type: integer }
        next_cursor: { type: [string, "null"] }
        snapshot: { type: boolean }
        permalink: { type: object, additionalProperties: true }
        response_time_ms: { type: number }
    Freshness:
      type: object
      description: >-
        Data-vintage / provenance block so a client can answer "as of when?". `as_of` is the
        datum's own time — the METAR observation time, TAF issue time, winds valid time, TFR
        capture time, or the AIRAC cycle's effective date — NOT the time-travel `meta.as_of`.
        `stale` is true when `age_seconds` exceeds that feed's expected cadence (e.g. a METAR
        older than ~75 min, or a realtime capture past its poll window). Distinct from
        `meta.cache.age_seconds`, which is the age of the cached envelope, not of the data.
      properties:
        as_of: { type: string, description: ISO-8601 vintage of the data., example: "2026-06-16T05:51:00Z" }
        age_seconds: { type: integer, description: Seconds between as_of and the response time., example: 220860 }
        stale: { type: boolean, description: True when older than the feed's expected cadence., example: true }
    Cache:
      type: object
      properties:
        status: { type: string, enum: [hit, miss, bypass] }
        layer: { type: string, example: kv }
        stored_at: { type: [string, "null"] }
        age_seconds: { type: [integer, "null"] }
    Upstream:
      type: object
      properties:
        status: { type: string, enum: [skipped, ok, error, stale], example: skipped }
    Cycle:
      type: object
      properties:
        type: { type: string, example: AIRAC }
        effective: { type: string, example: "2026-06-11" }
        next: { type: [string, "null"] }
    DataSourceCensus:
      type: object
      required: [sources, totals, generated_at]
      properties:
        sources:
          type: array
          items: { $ref: "#/components/schemas/DataSource" }
        totals:
          type: object
          properties:
            count: { type: integer, example: 11 }
            automated: { type: integer, example: 4 }
            stale: { type: integer, example: 0 }
            never: { type: integer, example: 0 }
        generated_at: { type: string, example: "2026-06-17T08:32:49.404Z" }
    DataSource:
      type: object
      description: One mirrored upstream product joined with its live mirror state. Non-PII by construction — aggregate counts + metadata only.
      required: [id, label, authority, scope, category, cadence, health, automated, pii, enabled]
      properties:
        id: { type: string, example: nasr }
        label: { type: string, example: "NASR — 28-Day Subscription" }
        authority: { type: string, example: faa }
        country: { type: [string, "null"], example: US }
        scope: { type: string, example: US }
        category: { type: string, enum: [batch, realtime, disabled] }
        dataset: { type: [string, "null"], example: nasr }
        entity_types: { type: array, items: { type: string }, example: [airport, runway, navaid, fix, airway] }
        upstream_url: { type: string, format: uri, example: "https://nfdc.faa.gov/webContent/28DaySub/" }
        upstream_label: { type: string, example: nfdc.faa.gov }
        cadence: { type: string, example: "Every 28 days (AIRAC)" }
        cadence_days: { type: [integer, "null"], example: 28 }
        automated: { type: boolean }
        pii: { type: boolean }
        enabled: { type: boolean }
        notes: { type: [string, "null"] }
        health: { type: string, enum: [fresh, due, stale, never, continuous, disabled] }
        last_pull:
          oneOf:
            - type: object
              properties:
                cycle: { type: string, example: "2026-06-11" }
                ingested_at: { type: [string, "null"] }
                ok: { type: boolean }
                archived: { type: boolean }
            - { type: "null" }
        age_days: { type: [number, "null"], example: 3.2 }
        history: { type: integer, example: 12 }
        diff: { $ref: "#/components/schemas/DataSourceDiff" }
        records:
          type: [object, "null"]
          additionalProperties: true
          description: Per-shard record counts (aggregate only — never PII rows). Null for realtime feeds; `{ error }` when the shard table is unavailable.
    DataSourceDiff:
      type: object
      description: The most-recent pull's field-level change counts (from change_log), grouped by kind.
      properties:
        added: { type: integer }
        removed: { type: integer }
        modified: { type: integer }
        total: { type: integer }
    NearestAirports:
      type: object
      required: [query, count, airports]
      properties:
        query:
          type: object
          required: [input, lat, lon, radius_nm, limit, details]
          properties:
            input: { type: string, enum: [coordinates, address] }
            lat: { type: number, minimum: -90, maximum: 90 }
            lon: { type: number, minimum: -180, maximum: 180 }
            address: { type: string }
            cc: { type: string, minLength: 2, maxLength: 2 }
            match: { type: [object, "null"], additionalProperties: true, description: Accuracy metadata from cca-geocode for address input. }
            matched: { type: [object, "null"], additionalProperties: true, description: Normalized address fields from cca-geocode. }
            radius_nm: { type: [number, "null"] }
            limit: { type: integer, minimum: 1, maximum: 25 }
            details: { type: boolean }
        count: { type: integer }
        airports:
          type: array
          items: { $ref: "#/components/schemas/NearestAirport" }
    NearestAirport:
      allOf:
        - $ref: "#/components/schemas/AirportSummary"
        - type: object
          required: [kind, airport_id, distance_nm, bearing_deg, detail_url]
          properties:
            kind: { type: string, const: airport }
            airport_id: { type: string, description: Canonical lookup identifier (`icao_id` when available, otherwise `ident`)., example: KJFK }
            distance_nm: { type: number, description: Great-circle distance from the resolved origin. }
            bearing_deg: { type: number, minimum: 0, maximum: 360, description: Initial true bearing from the resolved origin. }
            detail_url: { type: string, example: /api/v1/airport/KJFK }
            details:
              $ref: "#/components/schemas/Airport"
              description: Present only when `details=true`.
    AirportSummary:
      type: object
      properties:
        ident: { type: string, example: KJFK }
        icao_id: { type: [string, "null"] }
        local_id: { type: [string, "null"] }
        iata_id: { type: [string, "null"] }
        authority: { type: string, example: faa }
        country: { type: string, example: US }
        name: { type: [string, "null"] }
        type: { type: [string, "null"] }
        lat: { type: [number, "null"] }
        lon: { type: [number, "null"] }
        elevation_ft: { type: [integer, "null"] }
        municipality: { type: [string, "null"] }
        region: { type: [string, "null"] }
        cycle: { type: string }
    Airport:
      allOf:
        - $ref: "#/components/schemas/AirportSummary"
        - type: object
          properties:
            runways:
              type: array
              items: { $ref: "#/components/schemas/Runway" }
            frequencies:
              type: array
              description: |
                Per-airport radio frequencies. Present only for authorities that publish them
                (OurAirports — a product the US NASR feed never modeled); the key is omitted
                entirely for FAA airports.
              items: { $ref: "#/components/schemas/Frequency" }
    Frequency:
      type: object
      description: A single airport radio frequency (OurAirports airport-frequencies.csv).
      properties:
        type: { type: string, example: TWR, description: "Service, e.g. TWR | GND | ATIS | CTAF." }
        description: { type: [string, "null"], example: JFK TOWER }
        freq_mhz: { type: [number, "null"], example: 119.1, description: Frequency in MHz, as published. }
    Country:
      type: object
      description: ISO 3166-1 country reference (OurAirports countries.csv). Mirror-only.
      properties:
        code: { type: string, example: GB, description: ISO 3166-1 alpha-2. }
        name: { type: [string, "null"], example: United Kingdom }
        continent: { type: [string, "null"], example: EU }
        authority: { type: string, example: ourairports }
        wikipedia_link: { type: [string, "null"] }
        keywords: { type: [string, "null"] }
    Region:
      type: object
      description: ISO 3166-2 region reference (OurAirports regions.csv). Mirror-only.
      properties:
        code: { type: string, example: US-PA, description: ISO 3166-2 region code. }
        local_code: { type: [string, "null"], example: PA }
        name: { type: [string, "null"], example: Pennsylvania }
        continent: { type: [string, "null"], example: NA }
        iso_country: { type: [string, "null"], example: US, description: ISO 3166-1 alpha-2 this region belongs to. }
        authority: { type: string, example: ourairports }
        wikipedia_link: { type: [string, "null"] }
        keywords: { type: [string, "null"] }
    Airline:
      type: object
      description: OpenFlights airline (airlines.dat). Mirror-only; ODbL — attribute "OpenFlights".
      properties:
        airline_id: { type: string, example: "24", description: OpenFlights AirlineID (stable upstream identity). }
        name: { type: [string, "null"], example: American Airlines }
        alias: { type: [string, "null"] }
        iata: { type: [string, "null"], example: AA, description: 2-char IATA airline code. }
        icao: { type: [string, "null"], example: AAL, description: 3-char ICAO airline code. }
        callsign: { type: [string, "null"], example: AMERICAN }
        country: { type: [string, "null"], example: United States, description: Operating country (free text, as published). }
        active: { type: [string, "null"], example: "Y", description: "'Y' or 'N' as published upstream." }
        authority: { type: string, example: openflights }
    Route:
      type: object
      description: One OpenFlights scheduled route (routes.dat). Airport codes are IATA/ICAO as flown.
      properties:
        airline:
          type: [object, "null"]
          description: The operating carrier (resolved from the airline mirror); null when the route omits it.
          properties:
            code: { type: [string, "null"], example: AA }
            id: { type: [string, "null"], example: "24" }
            name: { type: [string, "null"], example: American Airlines }
            iata: { type: [string, "null"], example: AA }
            icao: { type: [string, "null"], example: AAL }
        src: { type: string, example: JFK, description: Source airport code as flown. }
        dst: { type: string, example: LAX, description: Destination airport code as flown. }
        codeshare: { type: boolean, description: True when operated as a codeshare. }
        stops: { type: integer, example: 0, description: Number of stops (0 = direct). }
        equipment:
          type: array
          items: { type: string }
          example: ["738", "32B"]
          description: Aircraft-type codes for the route.
    AirportRoutes:
      type: object
      description: OpenFlights routes touching an airport, split into departures and arrivals.
      properties:
        airport:
          type: object
          properties:
            id: { type: string, example: KJFK }
            icao: { type: [string, "null"], example: KJFK }
            iata: { type: [string, "null"], example: JFK }
            local: { type: [string, "null"], example: JFK }
            name: { type: [string, "null"], example: John F Kennedy International Airport }
            resolved: { type: boolean, description: Whether the ident matched an airport in the facilities mirror. }
            matched_codes:
              type: array
              items: { type: string }
              description: Every code the airport is known by, matched against route endpoints.
        direction: { type: string, enum: [departures, arrivals, both] }
        limit: { type: integer, example: 100 }
        counts:
          type: object
          properties:
            departures: { type: integer }
            arrivals: { type: integer }
        departures:
          type: array
          description: Present unless direction=arrivals.
          items: { $ref: "#/components/schemas/Route" }
        arrivals:
          type: array
          description: Present unless direction=departures.
          items: { $ref: "#/components/schemas/Route" }
    Runway:
      type: object
      properties:
        designator: { type: string, example: 04L/22R }
        length_ft: { type: [integer, "null"] }
        width_ft: { type: [integer, "null"] }
        surface: { type: [string, "null"] }
        le_ident: { type: [string, "null"] }
        le_lat: { type: [number, "null"] }
        le_lon: { type: [number, "null"] }
        le_heading: { type: [number, "null"] }
        he_ident: { type: [string, "null"] }
        he_lat: { type: [number, "null"] }
        he_lon: { type: [number, "null"] }
        he_heading: { type: [number, "null"] }
    MagVar:
      type: object
      description: NOAA WMM2025 geomagnetic field at a point — angles in degrees, intensities in nanotesla (nT). Pure compute from the in-repo coefficients; never persisted.
      properties:
        declination_deg: { type: number, example: -12.5751, description: Declination (magvar). East positive, west negative. }
        inclination_deg: { type: number, example: 65.5012, description: Inclination (dip angle). }
        horizontal_intensity_nt: { type: number, example: 21056.6, description: Horizontal intensity H (nT). }
        total_intensity_nt: { type: number, example: 50767.3, description: Total intensity F (nT). }
        north_nt: { type: number, example: 20553.2, description: X component — geodetic north (nT). }
        east_nt: { type: number, example: -4588.6, description: Y component — east (nT). }
        vertical_nt: { type: number, example: 46180.4, description: Z component — vertical/down (nT). }
        model: { type: string, example: WMM2025 }
        valid_from: { type: number, example: 2025.0 }
        valid_to: { type: number, example: 2030.0 }
        date_decimal: { type: number, example: 2026.4644, description: The decimal year actually evaluated (after any out-of-range clamp). }
        out_of_range: { type: boolean, example: false, description: True when the requested date fell outside 2025.0–2030.0 and was clamped to the nearest bound. }
        query:
          type: object
          description: Echo of the resolved request point.
          properties:
            lat: { type: number, example: 40.64 }
            lon: { type: number, example: -73.78 }
            alt_km: { type: number, example: 0 }
    Daylight:
      type: object
      description: |
        Solar events for a point and UTC date — computed (NOAA Solar Calculator / Meeus),
        never persisted. All event times are ISO-8601 UTC strings, `null` where the sun
        never reaches that altitude (polar day/night).
      properties:
        date: { type: string, example: "2026-06-21", description: UTC calendar date the events were computed for. }
        latitude: { type: number, example: 40.64 }
        longitude: { type: number, example: -73.78, description: East-positive longitude. }
        solar_noon: { type: [string, "null"], format: date-time, example: "2026-06-21T16:56:59.329Z", description: Sun transit (UTC). }
        sunrise: { type: [string, "null"], format: date-time, example: "2026-06-21T09:24:21.539Z", description: Sunrise — sun centre at -0.833° (UTC); null on a polar day/night. }
        sunset: { type: [string, "null"], format: date-time, example: "2026-06-22T00:29:36.732Z", description: Sunset — sun centre at -0.833° (UTC); null on a polar day/night. }
        civil_twilight_begin: { type: [string, "null"], format: date-time, description: Morning civil twilight begins — sun at -6° ascending (UTC). }
        civil_twilight_end: { type: [string, "null"], format: date-time, description: Evening civil twilight ends — sun at -6° descending (UTC). }
        nautical_twilight_begin: { type: [string, "null"], format: date-time, description: Morning nautical twilight begins — sun at -12° (UTC). }
        nautical_twilight_end: { type: [string, "null"], format: date-time, description: Evening nautical twilight ends — sun at -12° (UTC). }
        astronomical_twilight_begin: { type: [string, "null"], format: date-time, description: Morning astronomical twilight begins — sun at -18° (UTC). }
        astronomical_twilight_end: { type: [string, "null"], format: date-time, description: Evening astronomical twilight ends — sun at -18° (UTC). }
        day_length_seconds: { type: integer, example: 54315, description: Sunset minus sunrise, seconds. 86400 on a polar day, 0 on a polar night. }
        polar: { type: [string, "null"], enum: ["day", "night", null], example: null, description: "'day' when the sun is up all day, 'night' when down all day, else null." }
        sun_up_all_day: { type: boolean, example: false, description: True when the sun never sets (polar day). }
        sun_down_all_day: { type: boolean, example: false, description: True when the sun never rises (polar night). }
        faa_night_begin: { type: [string, "null"], format: date-time, description: "FAA night start (14 CFR 1.1) = end of evening civil twilight (= civil_twilight_end)." }
        faa_night_end: { type: [string, "null"], format: date-time, description: "FAA night end (14 CFR 1.1) = beginning of morning civil twilight (= civil_twilight_begin)." }
        faa_night_note: { type: string, description: Plain-language statement of the FAA-night convention. }
        query:
          type: object
          description: Echo of the resolved request point.
          properties:
            lat: { type: number, example: 40.64 }
            lon: { type: number, example: -73.78 }
    Atmosphere:
      type: object
      description: |
        US Standard Atmosphere 1976 (ISO 2533) properties at a geometric altitude —
        computed from the 7-layer piecewise model (0–86 km), never persisted.
      properties:
        altitude_m: { type: number, example: 11000, description: Geometric altitude (metres). }
        altitude_ft: { type: number, example: 36089.24, description: Geometric altitude (feet). }
        geopotential_m: { type: number, example: 10981.03, description: Geopotential altitude (metres). }
        temperature_K: { type: number, example: 216.65, description: Temperature (Kelvin). }
        temperature_C: { type: number, example: -56.5, description: Temperature (°C). }
        pressure_Pa: { type: number, example: 22632.1, description: Pressure (Pascals). }
        pressure_hPa: { type: number, example: 226.321, description: Pressure (hectopascals / millibars). }
        density_kgm3: { type: number, example: 0.36392, description: Air density (kg/m³). }
        speed_of_sound_ms: { type: number, example: 295.069, description: Speed of sound (m/s). }
        layer: { type: string, example: Tropopause, description: "Atmosphere layer name (Troposphere, Tropopause, Stratosphere 1/2, Stratopause, Mesosphere 1/2)." }
        model: { type: string, example: "US Standard Atmosphere 1976" }
        pressure_altitude_ft: { type: number, description: "Pressure altitude (ft) — only present when altimeter param provided." }
        pressure_altitude_m: { type: number, description: "Pressure altitude (m) — only present when altimeter param provided." }
        density_altitude_ft: { type: number, description: "Density altitude (ft) — only present when altimeter + oat params provided." }
        density_altitude_m: { type: number, description: "Density altitude (m) — only present when altimeter + oat params provided." }
        query:
          type: object
          description: Echo of the resolved request parameters.
          properties:
            altitude: { type: number }
            unit: { type: string }
    AircraftRegistration:
      type: object
      description: PII — owner name + mailing address are served at full fidelity. Additional registry fields are returned verbatim.
      additionalProperties: true
      properties:
        mark: { type: string, example: N12345 }
        authority: { type: string }
        country: { type: string }
        registrant: { type: [string, "null"] }
        status: { type: [string, "null"] }
        serial: { type: [string, "null"] }
        year_mfr: { type: [integer, "null"] }
        icao_hex: { type: [string, "null"] }
        mfr_mdl_code: { type: [string, "null"] }
        model: { type: [object, "null"], additionalProperties: true }
        cycle: { type: string }
        cca:
          type: [object, "null"]
          description: CCA identity overlay (cca-mci read-time layer) — present only when the operator is linked to an MCI.
          properties:
            operator_mci: { type: string, example: e6fd-d50432c8f14b }
            operator_mci_kind: { type: string, enum: [participant, non-participant, entity] }
    AirmanCertification:
      type: object
      description: PII — name, address and medical class served at full fidelity. Certificates + ratings included verbatim.
      additionalProperties: true
      properties:
        airman_id: { type: string, example: A0000014 }
        authority: { type: string }
        first_name: { type: [string, "null"] }
        last_name: { type: [string, "null"] }
        city: { type: [string, "null"] }
        state: { type: [string, "null"] }
        region: { type: [string, "null"] }
        certificates:
          type: array
          description: |
            Every current FAA certificate and all of its regular and aircraft type
            ratings; rating arrays are not capped. `issue_date` is populated only when
            the authoritative source publishes it. The live FAA Airmen Inquiry publishes
            the current certificate's full day; the downloadable releasable CERT files
            publish no issue date. Historical certificates no longer displayed by the
            live inquiry cannot be assigned an inferred date.
          items:
            type: object
            additionalProperties: true
            properties:
              cert_type: { type: string }
              level: { type: [string, "null"] }
              issue_date: { type: [string, "null"], format: date }
              issue_date_source: { type: [string, "null"], enum: [faa-inquiry-live, null] }
              expire_date: { type: [string, "null"] }
              ratings:
                type: array
                description: Regular category/class/instrument ratings plus aircraft type ratings, in full.
                items: { type: string }
        cycle: { type: string }
        cca:
          type: [object, "null"]
          description: CCA identity overlay (cca-mci read-time layer) — present only when the airman is linked to an MCI.
          properties:
            mci: { type: string, example: cgxc-2d82864bfda4 }
            mci_kind: { type: string, enum: [participant, non-participant] }
    MciLink:
      type: object
      description: One FAA record ↔ CCA MCI link (aero-core `mci_links`; CCA-local, merged at read time — never persisted into the mirror).
      properties:
        authority: { type: string, example: faa }
        entity_type: { type: string, enum: [airman, operator] }
        entity_id: { type: string, description: FAA UNIQUE ID (airman) or registration mark (operator)., example: A0000014 }
        mci: { type: string, example: cgxc-2d82864bfda4 }
        note: { type: [string, "null"] }
        author: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    MciIdentity:
      type: object
      description: Reverse lookup — every FAA record linked to one CCA MCI, grouped by type.
      properties:
        mci: { type: string, example: cgxc-2d82864bfda4 }
        kind: { type: string, enum: [participant, non-participant, entity] }
        airmen: { type: array, items: { type: object, additionalProperties: true } }
        operators: { type: array, items: { type: object, additionalProperties: true } }
    ChartProduct:
      type: object
      properties:
        product: { type: string, example: sectional-new-york }
        family: { type: [string, "null"], example: sectional }
        authority: { type: string }
        country: { type: string }
        title: { type: [string, "null"] }
        kind: { type: string, example: airac56 }
        tile_type: { type: string, example: webp }
        minzoom: { type: [integer, "null"] }
        maxzoom: { type: [integer, "null"] }
        bounds:
          type: [array, "null"]
          items: { type: number }
          description: "[w, s, e, n]"
        center:
          type: [array, "null"]
          items: { type: number }
          description: "[lon, lat, z]"
        cycle: { type: string }
        attribution: { type: [string, "null"] }
        tiles:
          type: object
          description: faacharts serving links (tiles served by cca-faacharts).
          properties:
            tilejson: { type: [string, "null"] }
            tiles: { type: [string, "null"], description: "{z}/{x}/{y} template" }
            latest_tilejson: { type: [string, "null"] }
            latest_tiles: { type: [string, "null"] }
        links:
          type: object
          additionalProperties: { type: string }
        sheets:
          type: array
          items: { type: object, additionalProperties: true }
    AirportFeatureCollection:
      type: object
      description: A raw GeoJSON FeatureCollection (RFC 7946) of airports. NOT wrapped in the fleet envelope.
      required: [type, features]
      properties:
        type: { type: string, enum: [FeatureCollection] }
        features:
          type: array
          items: { $ref: "#/components/schemas/AirportFeature" }
    AirportFeature:
      type: object
      required: [type, geometry, properties]
      properties:
        type: { type: string, enum: [Feature] }
        id: { type: string, example: KJFK }
        geometry:
          type: object
          properties:
            type: { type: string, enum: [Point] }
            coordinates:
              type: array
              items: { type: number }
              description: "[lon, lat]"
        properties: { $ref: "#/components/schemas/AirportSummary" }
    NavaidSummary:
      type: object
      properties:
        ident: { type: string, example: JFK }
        type: { type: [string, "null"], example: VOR/DME }
        name: { type: [string, "null"] }
        authority: { type: string, example: faa }
        country: { type: string, example: US }
        freq_khz: { type: [integer, "null"], example: 115900, description: Frequency normalised to kHz (VHF MHz → kHz; NDB already kHz). }
        lat: { type: [number, "null"] }
        lon: { type: [number, "null"] }
        elevation_ft: { type: [integer, "null"] }
        cycle: { type: string }
    NavaidType:
      type: object
      description: A co-located navaid type sharing the headline ident.
      properties:
        type: { type: string, example: NDB }
        name: { type: [string, "null"] }
        freq_khz: { type: [integer, "null"] }
        lat: { type: [number, "null"] }
        lon: { type: [number, "null"] }
        elevation_ft: { type: [integer, "null"] }
    Navaid:
      allOf:
        - $ref: "#/components/schemas/NavaidSummary"
        - type: object
          properties:
            colocated:
              type: array
              description: Extra navaid types sharing the ident (rare). Omitted when there is only one type.
              items: { $ref: "#/components/schemas/NavaidType" }
    FixSummary:
      type: object
      properties:
        name: { type: string, example: CEVIC }
        region: { type: [string, "null"], example: K6, description: ICAO region code. }
        authority: { type: string, example: faa }
        country: { type: string, example: US }
        lat: { type: [number, "null"] }
        lon: { type: [number, "null"] }
        cycle: { type: string }
    Fix:
      allOf:
        - $ref: "#/components/schemas/FixSummary"
        - type: object
          properties:
            colocated:
              type: array
              description: Extra rows for a name reused across ICAO regions (rare). Omitted otherwise.
              items:
                type: object
                properties:
                  region: { type: [string, "null"] }
                  lat: { type: [number, "null"] }
                  lon: { type: [number, "null"] }
    AirwayLeg:
      type: object
      properties:
        seq: { type: integer, example: 10 }
        from_fix: { type: [string, "null"], example: CEVIC }
        to_fix: { type: [string, "null"], example: MERIT }
    AirwaySummary:
      type: object
      properties:
        designator: { type: string, example: V25 }
        authority: { type: string, example: faa }
        country: { type: string, example: US }
        cycle: { type: string }
    Airway:
      allOf:
        - $ref: "#/components/schemas/AirwaySummary"
        - type: object
          properties:
            legs:
              type: array
              items: { $ref: "#/components/schemas/AirwayLeg" }
    NavaidFeatureCollection:
      type: object
      description: A raw GeoJSON FeatureCollection (RFC 7946) of navaids. NOT wrapped in the fleet envelope.
      required: [type, features]
      properties:
        type: { type: string, enum: [FeatureCollection] }
        features:
          type: array
          items: { $ref: "#/components/schemas/NavaidFeature" }
    NavaidFeature:
      type: object
      required: [type, geometry, properties]
      properties:
        type: { type: string, enum: [Feature] }
        id: { type: string, example: JFK }
        geometry:
          type: object
          properties:
            type: { type: string, enum: [Point] }
            coordinates:
              type: array
              items: { type: number }
              description: "[lon, lat]"
        properties: { $ref: "#/components/schemas/NavaidSummary" }
    FixFeatureCollection:
      type: object
      description: A raw GeoJSON FeatureCollection (RFC 7946) of fixes. NOT wrapped in the fleet envelope.
      required: [type, features]
      properties:
        type: { type: string, enum: [FeatureCollection] }
        features:
          type: array
          items: { $ref: "#/components/schemas/FixFeature" }
    FixFeature:
      type: object
      required: [type, geometry, properties]
      properties:
        type: { type: string, enum: [Feature] }
        id: { type: string, example: CEVIC }
        geometry:
          type: object
          properties:
            type: { type: string, enum: [Point] }
            coordinates:
              type: array
              items: { type: number }
              description: "[lon, lat]"
        properties: { $ref: "#/components/schemas/FixSummary" }
    AirwayFeatureCollection:
      type: object
      description: A raw GeoJSON FeatureCollection (RFC 7946) of airways as LineStrings. NOT wrapped in the fleet envelope.
      required: [type, features]
      properties:
        type: { type: string, enum: [FeatureCollection] }
        features:
          type: array
          items: { $ref: "#/components/schemas/AirwayFeature" }
    AirwayFeature:
      type: object
      required: [type, geometry, properties]
      properties:
        type: { type: string, enum: [Feature] }
        id: { type: string, example: V25 }
        geometry:
          type: object
          properties:
            type: { type: string, enum: [LineString] }
            coordinates:
              type: array
              items:
                type: array
                items: { type: number }
              description: "Ordered [lon, lat] vertices."
        properties: { $ref: "#/components/schemas/AirwaySummary" }
    Obstacle:
      type: object
      description: A single FAA Digital Obstacle File (DOF) record. The cycle is reported in `meta.cycle`, not the body.
      properties:
        obstacle_id: { type: string, example: "11-024069" }
        authority: { type: string, example: faa }
        country: { type: [string, "null"], example: US }
        oas_code: { type: [string, "null"], example: "11" }
        obstacle_number: { type: [string, "null"], example: "024069" }
        verification_status: { type: [string, "null"], example: O, description: "FAA DOF verification status: O = verified, U = unverified (README 'VERIFICATION STATUS')." }
        state: { type: [string, "null"], example: DC }
        city: { type: [string, "null"], example: WASHINGTON }
        lat: { type: [number, "null"], example: 38.8104556 }
        lon: { type: [number, "null"], example: -77.0259056 }
        type: { type: [string, "null"], example: BUILDING }
        quantity: { type: [integer, "null"], example: 1 }
        agl_ft: { type: [integer, "null"], description: Height above ground level (ft). }
        amsl_ft: { type: [integer, "null"], description: Elevation above mean sea level (ft). }
        lighting: { type: [string, "null"] }
        marking: { type: [string, "null"] }
        horiz_acc: { type: [string, "null"] }
        vert_acc: { type: [string, "null"] }
        faa_study: { type: [string, "null"] }
        action: { type: [string, "null"], example: C, description: "A = add, C = change." }
        action_date: { type: [string, "null"], example: "2025045", description: Julian (YYYYDDD) action date. }
    ObstacleFeatureCollection:
      type: object
      description: A raw GeoJSON FeatureCollection (RFC 7946) of DOF obstacles. NOT wrapped in the fleet envelope.
      required: [type, features]
      properties:
        type: { type: string, enum: [FeatureCollection] }
        features:
          type: array
          items: { $ref: "#/components/schemas/ObstacleFeature" }
    ObstacleFeature:
      type: object
      required: [type, geometry, properties]
      properties:
        type: { type: string, enum: [Feature] }
        id: { type: string, example: "faa/11-024069" }
        geometry:
          type: object
          properties:
            type: { type: string, enum: [Point] }
            coordinates:
              type: array
              items: { type: number }
              description: "[lon, lat]"
        properties:
          type: object
          properties:
            obstacle_id: { type: string }
            type: { type: string }
            agl_ft: { type: [integer, "null"] }
            amsl_ft: { type: [integer, "null"] }
            lighting: { type: [string, "null"] }
            verification_status: { type: [string, "null"] }
            authority: { type: string }
            country: { type: [string, "null"] }
            cycle: { type: string }
    ProcedureSummary:
      type: object
      description: One coded procedure as listed under `/airport/{id}/procedures` — a compact catalog row, not the full leg set.
      properties:
        type: { type: string, enum: [SID, STAR, IAP] }
        ident: { type: string, example: DEEZZ6 }
        transition_count: { type: integer, example: 6 }
        leg_count: { type: integer, example: 31 }
        transitions:
          type: array
          description: Per-transition summary (idents + kinds + leg counts), no legs.
          items:
            type: object
            properties:
              transition: { type: [string, "null"], example: RW04L, description: Transition ident; `null`/empty = the common segment. }
              kind: { type: string, enum: [common, runway, all, enroute], example: runway }
              leg_count: { type: integer, example: 3 }
        url: { type: string, example: /api/v1/procedure/KJFK/SID/DEEZZ6, description: Same-origin per-procedure detail path. }
    Procedure:
      type: object
      description: |
        A single CIFP / ARINC 424 coded instrument procedure (SID / STAR / IAP) with its
        ordered legs grouped by transition. The AIRAC cycle is reported in `meta.cycle`,
        not the body. Leg fixes are referenced by ident + section + region; coordinates are
        intentionally absent (resolved later from the NASR fix/navaid mirror).
      properties:
        airport_ident: { type: [string, "null"], example: KJFK }
        region: { type: [string, "null"], example: K6, description: ARINC 424 ICAO region code. }
        type: { type: [string, "null"], enum: [SID, STAR, IAP, null], example: SID }
        ident: { type: [string, "null"], example: DEEZZ6 }
        transition_count: { type: integer, example: 6 }
        leg_count: { type: integer, example: 31 }
        transitions:
          type: array
          items: { $ref: "#/components/schemas/ProcedureTransition" }
    ProcedureTransition:
      type: object
      description: A transition within a procedure — a runway transition (`RW04L`), a named enroute transition (`CANDR`), or the common segment.
      properties:
        transition: { type: [string, "null"], example: RW04L, description: Transition ident; `null`/empty = the common segment. }
        kind: { type: string, enum: [common, runway, all, enroute], example: runway }
        route_type: { type: [string, "null"], example: "4", description: ARINC 424 route-type code. }
        leg_count: { type: integer, example: 3 }
        legs:
          type: array
          items: { $ref: "#/components/schemas/ProcedureLeg" }
    ProcedureLeg:
      type: object
      description: One ARINC 424 procedure leg, in sequence order.
      properties:
        sequence: { type: integer, example: 10, description: ARINC 424 3-digit sequence number. }
        fix:
          type: [object, "null"]
          description: The leg's terminating fix (waypoint / navaid / runway), or `null` for legs with no fix (e.g. VA / VM headings).
          properties:
            ident: { type: string, example: PONAE }
            section: { type: [string, "null"], example: D, description: ARINC 424 section of the fix record. }
            subsection: { type: [string, "null"], example: B }
            region: { type: [string, "null"], example: K6 }
        path_and_termination:
          type: object
          description: ARINC 424 path-and-termination — how the leg is flown and what ends it.
          properties:
            code: { type: [string, "null"], example: DF, description: "2-letter code (IF/TF/CF/DF/VA/VM/FM/RF/HM/HA/HF/CA/CD/CI/CR/AF/PI…)." }
            label: { type: [string, "null"], example: Direct to fix, description: Human-readable expansion of the code. }
        turn: { type: [string, "null"], enum: [left, right, either, null], example: left }
        course_deg: { type: [number, "null"], example: 43.8, description: Magnetic course (degrees). }
        distance_nm: { type: [number, "null"], example: 4.2, description: Along-track leg distance (nm). }
        hold_time_min: { type: [number, "null"], description: Holding leg time (minutes), when the distance field encodes time. }
        altitude:
          type: [object, "null"]
          description: Altitude constraint, or `null` when unconstrained.
          properties:
            description: { type: [string, "null"], example: "+", description: "ARINC 424 altitude descriptor: `+` at-or-above, `-` at-or-below, blank at, `B` between." }
            label: { type: [string, "null"], example: At or above, description: Human-readable expansion of the descriptor. }
            value1: { $ref: "#/components/schemas/Altitude" }
            value2: { $ref: "#/components/schemas/Altitude" }
        speed_limit_kt: { type: [integer, "null"], example: 250 }
        vertical_angle_deg: { type: [number, "null"], example: -3.0, description: Vertical path angle (degrees, negative = descent). }
        recommended_navaid:
          type: [object, "null"]
          properties:
            ident: { type: string, example: JFK }
            region: { type: [string, "null"], example: K6 }
            theta_deg: { type: [number, "null"], description: Bearing from the recommended navaid (degrees). }
            rho_nm: { type: [number, "null"], description: Distance from the recommended navaid (nm). }
        waypoint_desc: { type: [string, "null"], example: "E  C", description: ARINC 424 4-char waypoint description code. }
        rnp: { type: [string, "null"], description: Required navigation performance value, when published. }
    Altitude:
      type: [object, "null"]
      description: A single decoded altitude — feet, or a flight level (which also carries its feet equivalent), or the raw token when unrecognised.
      properties:
        feet: { type: [integer, "null"], example: 520 }
        flight_level: { type: [integer, "null"], example: 230, description: Present only when the altitude was expressed as `FLnnn`. }
        raw: { type: [string, "null"], description: The raw token, present only when it could not be decoded to feet / flight level. }
    PermalinkRequest:
      type: object
      properties:
        kind: { type: string, enum: [result, sequence], default: result }
        path: { type: string, example: "/api/v1/airport/KJFK?as_of=2026-05-14" }
        collection: { type: string }
        id: { type: string }
        from: { type: string }
        to: { type: string }
