openapi: 3.1.0
info:
  title: CCA Schools API
  version: '1.0'
  description: US education institutions — identity graph + intelligence API. Every JSON response uses the fleet envelope.
servers:
  - url: /api/v1
security: []
tags:
  - name: service
  - name: discovery
  - name: entities
  - name: history
  - name: assessments
  - name: admin
paths:
  /health:
    get:
      tags: [service]
      summary: Service banner and docs links
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
  /status:
    get:
      tags: [service]
      summary: Coverage, bindings, authorities, datasets and history counters
      parameters:
        - { name: summary, in: query, schema: { type: boolean, default: false }, description: Return only the coverage and history counters used by lightweight dashboards }
      responses:
        '200': { description: OK, content: { application/json: { schema: { allOf: [ { $ref: '#/components/schemas/Envelope' }, { type: object, properties: { data: { $ref: '#/components/schemas/Status' } } } ] } } } }
  /search:
    get:
      tags: [discovery]
      summary: Search institutions by name, MCI, place, website domain or external identifier
      description: >
        General discovery search plus the service-binding POI provider used by cca-geocode.
        For the provider contract, call `env.SCHOOLS.fetch()` with a synthetic internal URL,
        `type=poi`, `q`, `limit` (1-20), and optional ISO alpha-2 `cc`; never call the public
        schools hostname Worker-to-Worker. POI mode returns only public/private schools and
        postsecondary institutions, uses a bounded CORE FTS candidate query, and preserves the
        standard fleet envelope. The MCI is the permanent `id`/`poi_id`.
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, minLength: 2, maxLength: 160 }, description: 'Search text — a name, "City, ST", ZIP, MCI, external id, or a website domain (e.g. harvard.edu)' }
        - { name: type, in: query, schema: { type: string, enum: [poi, school, school-public, school-private, district, college, postsecondary, agency, system] }, description: '`poi` is the cca-geocode provider profile and combines school-public, school-private and postsecondary' }
        - { name: state, in: query, schema: { type: string, minLength: 2, maxLength: 6 }, description: 'Subdivision code — US state, CA province, GB nation (ENG), AU state, NZ region (NZ-AUK), or IE county (IE-D). Codes collide across countries (WA!): pair with country to disambiguate' }
        - { name: country, in: query, schema: { type: string, enum: [US, CA, GB, UK, AU, NZ, IE] }, description: 'Narrow to one country (ISO alpha-2; UK accepted as alias of GB). Omit to search globally' }
        - { name: cc, in: query, schema: { type: string, minLength: 2, maxLength: 2 }, description: 'ISO 3166-1 alpha-2 country filter used by service-binding POI callers' }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 }, description: 'POI mode: maximum 20 and default 10. Other search modes: maximum 100 and default 25.' }
      responses:
        '200': { description: Search results, content: { application/json: { schema: { allOf: [ { $ref: '#/components/schemas/Envelope' }, { type: object, properties: { data: { type: object, properties: { classification: { type: object }, count: { type: integer }, results: { type: array, items: { oneOf: [ { $ref: '#/components/schemas/SearchResult' }, { $ref: '#/components/schemas/PoiSearchResult' } ] } } } } } } ] } } } }
        '400': { description: q, limit, type, cc or country validation error, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
  /query:
    get:
      tags: [discovery]
      summary: Structured list queries — multi-state, enrollment and district funding filters
      description: >
        Set queries over the mirrored universe: "public schools in TX or CA with over 2,000 students",
        "districts in NY with total funding ≥ $500M", "districts spending over $25k per student",
        "government schools in NSW or VIC" (country=AU). Filters AND together; multiple states OR.
        Funding filters use the district-level Census F-33 mirror (`type=district`, US-only): totals
        are raw mirrored dollars, per-student figures are derived at read time (spend or revenue ÷
        the F-33 fall membership). Money params accept k/m/b suffixes ('500k', '1.5m', '2b').
        At least one filter is required.
      parameters:
        - { name: type, in: query, schema: { type: string, enum: [school, school-public, school-private, district, college, postsecondary], default: school } }
        - { name: country, in: query, schema: { type: string, enum: [US, CA, GB, UK, AU, NZ, IE], default: US }, description: 'ISO alpha-2 country (UK = alias of GB). Region codes are validated against THIS country and results are scoped to its authorities — so state=WA means Washington under US and Western Australia under AU' }
        - { name: state, in: query, schema: { type: string }, description: 'Subdivision code(s), OR-ed — comma list ("TX,CA" / "NSW,VIC" / "ON,QC" / "ENG") and/or repeated ?state=' }
        - { name: city, in: query, schema: { type: string } }
        - { name: zip, in: query, schema: { type: string } }
        - { name: level, in: query, schema: { type: string }, description: elementary | middle | secondary | combined | other (K-12); 4-year | 2-year | less-2-year (college) }
        - { name: enrollment_min, in: query, schema: { type: string }, description: Minimum students (suffixes allowed, e.g. 2k) }
        - { name: enrollment_max, in: query, schema: { type: string } }
        - { name: rev_total_min, in: query, schema: { type: string }, description: 'District total revenue ≥ $ (alias: funding_total_min). Also *_max, and rev_federal/rev_state/rev_local' }
        - { name: exp_current_min, in: query, schema: { type: string }, description: 'District current spending ≥ $. Also exp_total / exp_instruction, each with _min/_max' }
        - { name: exp_per_pupil_min, in: query, schema: { type: string }, description: 'Spending per student ≥ $ (aliases: per_pupil_min, spend_per_pupil_min). Also *_max' }
        - { name: rev_per_pupil_min, in: query, schema: { type: string }, description: 'Funding per student ≥ $ (alias: funding_per_pupil_min). Also *_max' }
        - { name: include, in: query, schema: { type: string }, description: '`finance` attaches the district finance block without filtering on it' }
        - { name: cycle, in: query, schema: { type: string }, description: Pin the F-33 fiscal cycle ('YYYY-YYYY'); default = latest mirrored }
        - { name: sort, in: query, schema: { type: string, default: '-enrollment' }, description: '[-]enrollment | name | state | (district) rev_total, exp_current, exp_per_pupil, rev_per_pupil, …' }
        - { name: limit, in: query, schema: { type: integer, default: 25, maximum: 100 } }
        - { name: offset, in: query, schema: { type: integer, default: 0, maximum: 10000 } }
      responses:
        '200': { description: 'Matching institutions with total/count/has_more; district rows carry a `finance` block (totals + derived per-student) when funding was filtered/sorted/included', content: { application/json: { schema: { allOf: [ { $ref: '#/components/schemas/Envelope' }, { type: object, properties: { data: { type: object, properties: { query: { type: object }, total: { type: integer }, count: { type: integer }, limit: { type: integer }, offset: { type: integer }, has_more: { type: boolean }, results: { type: array, items: { $ref: '#/components/schemas/SearchResult' } } } } } } ] } } } }
        '400': { description: Validation error (unknown type/state/sort, funding filter on a non-district type, no filters) }
  /targeted-school-violence/{school}:
    get:
      tags: [discovery]
      summary: Cross-reference a school with Targeted School Violence incident records
      description: >
        Resolves a CCA school MCI or external school identifier, then calls cca-violence-api through
        a Cloudflare service binding. The provider's school route mixes explicit relationships
        with neutral proximity mappings, so this endpoint exhausts provider pagination and returns
        only `school_relationship.school_related_status=explicit`. Proximity rows are excluded.
        Coverage is incomplete,
        automated fields can be wrong, and sources can conflict; callers must read
        `data.interpretation` and the linked limitations before drawing conclusions.
      parameters:
        - { name: school, in: path, required: true, schema: { type: string }, description: 'CCA school MCI or external school identifier (use ?scheme= for an unqualified external id)' }
        - { name: scheme, in: query, schema: { type: string }, description: 'Identifier scheme, e.g. nces' }
        - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 100 } }
      responses:
        '200': { description: 'School, relationship summary, bounded incident list, pagination and mandatory interpretation warnings', content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '400': { description: Identifier is not a school or is malformed }
        '404': { description: School not found }
        '429': { description: Cross-service lookup rate-limited }
        '502': { description: Violence service unavailable or invalid response }
  /nearest:
    get:
      tags: [discovery]
      summary: Find institutions near a coordinate
      description: >
        Returns map-ready schools, colleges, districts, systems and agencies ordered by distance.
        Every result has validated `lat`/`lon` plus `point` precision/provenance. Source coordinates
        are labeled `source-reported`, not rooftop. When `zip` (preferred) or `city` + `state` is
        supplied, records missing source coordinates are street-geocoded through the bound
        `cca-geocode` Worker; postal/place-centroid fallbacks are omitted rather than represented as
        institution points. Missing-coordinate discovery is bounded to 100 records and reports
        `geocode_candidates_truncated` when more matched the hint. `mci` is the permanent canonical
        identity; source identifiers are aliases.
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number } }
        - { name: lon, in: query, required: true, schema: { type: number } }
        - { name: radius_km, in: query, schema: { type: number, default: 10 } }
        - { name: zip, in: query, schema: { type: string }, description: ZIP/postal hint used only to discover and street-geocode nearby records whose source coordinates are missing }
        - { name: city, in: query, schema: { type: string }, description: City hint for missing-coordinate discovery when zip is unavailable; use with state }
        - { name: state, in: query, schema: { type: string, minLength: 2, maxLength: 2 }, description: USPS state code used with city }
        - { name: type, in: query, schema: { $ref: '#/components/schemas/EntityType' } }
        - { name: limit, in: query, schema: { type: integer, default: 25 } }
      responses:
        '200': { description: Map-ready nearby results with canonical matching fields, content: { application/json: { schema: { $ref: '#/components/schemas/NearbyEnvelope' } } } }
  /nearby:
    get:
      tags: [discovery]
      summary: Geocode an address or ZIP, then return the nearest institutions
      description: >
        Resolves a free-form `q` (or explicit `address` / `zip`) through the `GEOCODE` service binding
        and returns the nearest institutions, distance-sorted. `data.geocode` echoes the search center,
        including precision and source metadata. A ZIP centroid may center this search and is explicitly
        marked approximate; it is never used as an exact institution point.
      parameters:
        - { name: q, in: query, description: 'Free-form address or 5-digit ZIP (e.g. "30 Fair St, Carmel NY" or "10512")', schema: { type: string } }
        - { name: address, in: query, schema: { type: string } }
        - { name: zip, in: query, schema: { type: string } }
        - { name: radius_km, in: query, schema: { type: number, default: 16 } }
        - { name: type, in: query, schema: { $ref: '#/components/schemas/EntityType' } }
        - { name: limit, in: query, schema: { type: integer, default: 25 } }
      responses:
        '200': { description: Map-ready nearby results with geocode echo, content: { application/json: { schema: { $ref: '#/components/schemas/NearbyEnvelope' } } } }
        '404': { description: Address/ZIP could not be located }
  /changes:
    get:
      tags: [history]
      summary: Global field-level change feed
      parameters:
        - { name: limit, in: query, schema: { type: integer, default: 100 } }
        - { name: type_concrete, in: query, schema: { $ref: '#/components/schemas/EntityType' } }
      responses:
        '200': { description: Change feed, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
  /exams:
    get:
      tags: [discovery]
      summary: Curated catalog of state assessment programs (which exams apply to which states)
      description: >
        Returns the CCA exam catalog — the standardized assessment programs each US state (and the
        federal government) requires, e.g. New York Regents, Texas STAAR, California CAASPP. Each
        program reports its subjects, grade level, vendor, proficiency standard and whether CCA
        already mirrors its school-level results (`mirrored: true`, with the backing `dataset`).
        Retired/historical tests a state no longer gives (e.g. Florida FCAT, Texas TAKS) are included
        with `retired: true`, their administration `years`, and the `replaced_by` successor program.
        `data.summary` rolls up program/state/mirrored counts (current vs retired).
      parameters:
        - { name: state, in: query, description: USPS state code (e.g. NY, TX) — federal programs are always included, schema: { type: string, minLength: 2, maxLength: 2 } }
        - { name: fips, in: query, description: Numeric state FIPS code, schema: { type: string } }
        - { name: authority, in: query, schema: { type: string } }
        - { name: subject, in: query, schema: { type: string, enum: [ela, math, science, social, writing] } }
        - { name: type, in: query, schema: { type: string, enum: [grades-3-8, comprehensive, high-school-eoc, high-school-general, high-school-exit, college-entrance, science] } }
        - { name: level, in: query, schema: { type: string, enum: [elementary-middle, high, all] } }
        - { name: status, in: query, description: 'Filter by lifecycle: current (active programs only) or retired (historical only). Omit for both.', schema: { type: string, enum: [current, retired] } }
        - { name: mirrored, in: query, description: 'Filter to programs we do (true) or do not (false) mirror', schema: { type: boolean } }
      responses:
        '200': { description: Exam catalog, content: { application/json: { schema: { allOf: [ { $ref: '#/components/schemas/Envelope' }, { type: object, properties: { data: { type: object, properties: { count: { type: integer }, summary: { type: object }, programs: { type: array, items: { $ref: '#/components/schemas/ExamProgram' } } } } } } ] } } } }
  /exams/{key}:
    get:
      tags: [discovery]
      summary: A single exam program by id, or all programs for a state / FIPS / authority
      description: >
        If `key` matches a program id (e.g. `ny-regents`) returns that one program. Otherwise a
        2-letter `key` is treated as a state code, an all-digit `key` as a FIPS code, and anything
        else as an authority — returning every matching program (federal programs included for a state).
      parameters:
        - { name: key, in: path, required: true, schema: { type: string }, examples: { program: { value: ny-regents }, state: { value: NY }, fips: { value: '48' } } }
      responses:
        '200': { description: Program or program list, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '404': { description: No program/state/authority matches the key }
  /resolve/{scheme}/{value}:
    get:
      tags: [discovery]
      summary: Resolve an external identifier to the canonical MCI entity bundle
      parameters:
        - { name: scheme, in: path, required: true, schema: { type: string }, examples: { nces: { value: nces }, unitid: { value: ipeds-unitid } } }
        - { name: value, in: path, required: true, schema: { type: string } }
      responses:
        '200': { description: Entity bundle, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
  /domain/{domain}:
    get:
      tags: [discovery]
      summary: Resolve a website domain to the institution(s) that own it
      description: >-
        Looks up a website domain (e.g. `harvard.edu`, `www.lwsd.org`) and returns the linked
        institution(s) with their canonical MCI. A pasted URL or a subdomain is accepted — the host
        is canonicalised and, on a miss, walked up to its registrable domain (reported as a `parent`
        match). Resolution is graph-first (the `domain` alias in the identifier graph), falling back
        to a scan of the mirrored website fields when no edge exists yet.


        **Returns both sides of the hierarchy.** A website usually belongs to a parent organisation
        while its schools/campuses share it, so the response surfaces the whole family, each result
        tagged `via`:

        * `domain` — the institution that owns the looked-up website.

        * `parent` — the org it rolls up to (a school's **district**, a college's **system**/parent
          campus), so a school's own domain still surfaces its district.

        * `child` — when the looked-up domain belongs to a **district/system**, its member
          schools/campuses (capped). This is the common K-12 case: **most schools reuse their
          district's domain**, so the district is the canonical owner and its schools are surfaced
          here. Schools that sit on a **subdomain** of the district site (`high.lwsd.org`,
          `middle.lwsd.org`) attribute the bare district domain (`lwsd.org`) to the **district**,
          while each school keeps its own subdomain edge — so both `lwsd.org` (→ district + schools)
          and `high.lwsd.org` (→ that school) resolve. State-issued `*.k12.<st>.us` hosts
          (`springfield.k12.or.us`) are always treated as the district's.

        Each non-direct result carries the governance `relation_type` and `related_to`. When exactly
        one institution owns the domain, its full entity bundle is inlined as `data.resolved`.


        Domain edges are cross-referenced from public sources on a **deterministic NCES id** (never a
        name match): the HIFLD national Public Schools layer (NCES school id + district LEAID +
        website — the K-12 coverage backbone) and Wikidata (`official website` by NCESSCH/LEAID, CC0),
        refreshed daily, on top of the website URLs already mirrored from IPEDS/CCD/state feeds.
        Shared multi-tenant hosts are deliberately **excluded** (never linked to an institution):
        website builders (`*.wixsite.com`, `*.weebly.com`, Google Sites), K-12 CMS vendors
        (`*.schoolwires.net`, `*.sharpschool.com`, `*.schoolloop.com`, Finalsite…), social profiles,
        whole-**state** `.gov` portals (`alabama.gov`, `ca.gov`), and any host shared across many
        districts (a CMS platform or charter-management network) — though city/county portals such as
        `nyc.gov` are kept. As a result most public schools resolve through their district's domain.


        **`domain_owner` — whose domain is this.** Alongside the result list the response names the
        institution that owns the *registrable* domain (`registrable_domain`, e.g. `albertk12.org`).
        For K-12 this is almost always the **district**; for higher-ed a university **system**/parent
        campus — because member schools/campuses reuse it. `domain_owner.scope` is `district`,
        `system`, or `institution` (a standalone school/college that owns its own domain), and
        `domain_owner.shared` is true when members sit on it. Even when you look up a single school's
        own subdomain (`redmond.lwsd.org`), `domain_owner` still resolves to its district, and `note`
        states the relationship in plain language.


        **Pick your payload (performance).** `?include=` is a comma list of the sections to return:
        `owner` (the `domain_owner` + `note`), `results` (the linked-institution list), and `resolved`
        (the full bundle of the headline match). `resolved` is the only heavy section — it triggers a
        KV/D1/R2 entity fetch — so a caller that only needs identity + the owning district can request
        `?include=owner,results` (or `?include=owner`) for a faster, smaller response. The default is
        all three. `?fields=` further narrows the `resolved` bundle to the listed top-level keys.
      parameters:
        - { name: domain, in: path, required: true, schema: { type: string }, examples: { college: { value: harvard.edu }, district: { value: www.lwsd.org }, school_subdomain: { value: redmond.lwsd.org }, url: { value: 'https://example.edu/about' } } }
        - { name: type, in: query, required: false, schema: { type: string, enum: [school, district, college, agency, system] }, description: Restrict results to one headline type }
        - { name: include, in: query, required: false, schema: { type: string, example: 'owner,results' }, description: 'Comma list of sections to return: owner, results, resolved. Omit resolved to skip the heavy entity-bundle fetch. Defaults to all three.' }
        - { name: expand, in: query, required: false, schema: { type: boolean, default: true }, description: Append the governance family (a school's district / a college's system, and a district/system's member schools/campuses). Set 0 to return only direct domain matches. }
        - { name: fields, in: query, required: false, schema: { type: string }, description: Comma list of top-level keys to keep in the `resolved` bundle (only applies when `resolved` is included). }
      responses:
        '200':
          description: One or more institutions linked to the domain
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          query: { type: string }
                          domain: { type: string, description: Canonicalised host that was looked up }
                          registrable_domain: { type: string, description: 'The bare registrable domain behind the lookup (`redmond.lwsd.org` → `lwsd.org`) — the domain a district/system actually owns and that its schools/campuses share.' }
                          matched_domain: { type: string, description: The graph domain that actually matched (may be a parent of `domain`) }
                          match: { type: string, enum: [exact, parent] }
                          source: { type: string, enum: [identifier-graph, snapshot-scan], description: Whether the answer came from the identifier graph or the snapshot fallback }
                          count: { type: integer }
                          domain_owner:
                            description: 'The institution that owns the registrable domain — the DISTRICT (K-12) or SYSTEM/parent campus (higher-ed) whose members share it, or the standalone institution itself. Present unless `include` omits `owner`.'
                            oneOf:
                              - type: object
                                properties:
                                  mci: { type: string }
                                  entity_type: { type: string }
                                  name: { type: string }
                                  city: { type: string, nullable: true }
                                  state: { type: string, nullable: true }
                                  status: { type: string, nullable: true }
                                  matched_domain: { type: string, description: The registrable domain this owner holds (or is inferred to hold via governance) }
                                  scope: { type: string, enum: [district, system, institution], description: 'Makes "this is a district/system domain" explicit.' }
                                  shared: { type: boolean, description: True when member schools/campuses sit on this domain }
                                  via: { type: string, enum: [domain, parent], description: Whether the owner was a direct domain edge or surfaced as the headline's governance parent }
                                  relation_type: { type: string, nullable: true }
                              - type: 'null'
                          note: { type: string, nullable: true, description: 'Plain-language statement of whose domain this is (e.g. "lwsd.org is a school-district domain operated by Lake Washington SD …"). Present with the `owner` section.' }
                          results:
                            type: array
                            description: All linked institutions — the website owner(s) plus their governance parent(s) (district/system) and, for a district/system domain, member school(s)/campus(es). Ordered with the best direct match first. Present unless `include` omits `results`.
                            items: { $ref: '#/components/schemas/SearchResult' }
                          resolved:
                            description: Full entity bundle for the headline match. Included by default; omitted (and the heavy fetch skipped) when `include` excludes `resolved`.
                            oneOf: [ { $ref: '#/components/schemas/EntityBundle' }, { type: 'null' } ]
        '400': { description: Not a valid domain, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '404': { description: Valid domain with no linked institution, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
  /{collection}/{id}:
    get:
      tags: [entities]
      summary: Fetch a school, district, college, agency, or system bundle by MCI or accepted identifier
      parameters:
        - { name: collection, in: path, required: true, schema: { type: string, enum: [school, district, college, agency, system] } }
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: view, in: query, schema: { type: string, enum: [full, core], default: full }, description: '`core` returns the durable institution profile without optional assessment, enrollment, or finance enrichment; intended for fast initial rendering before lazy detail requests' }
      responses:
        '200': { description: Entity bundle, content: { application/json: { schema: { allOf: [ { $ref: '#/components/schemas/Envelope' }, { type: object, properties: { data: { $ref: '#/components/schemas/EntityBundle' } } } ] } } } }
  /{collection}/{id}/identifiers:
    get:
      tags: [entities]
      summary: Identifier graph with provenance
      parameters: [ { $ref: '#/components/parameters/Collection' }, { $ref: '#/components/parameters/Id' } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } }
  /{collection}/{id}/relations:
    get:
      tags: [entities]
      summary: Relationship graph parents and children
      parameters: [ { $ref: '#/components/parameters/Collection' }, { $ref: '#/components/parameters/Id' } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } }
  /{collection}/{id}/members:
    get:
      tags: [entities]
      summary: Alias for relations
      parameters: [ { $ref: '#/components/parameters/Collection' }, { $ref: '#/components/parameters/Id' } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } }
  /{collection}/{id}/history:
    get:
      tags: [history]
      summary: Per-cycle snapshot index
      parameters: [ { $ref: '#/components/parameters/Collection' }, { $ref: '#/components/parameters/Id' } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } }
  /{collection}/{id}/changes:
    get:
      tags: [history]
      summary: Per-entity field changes
      parameters: [ { $ref: '#/components/parameters/Collection' }, { $ref: '#/components/parameters/Id' } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } }
  /{collection}/{id}/snapshots:
    get:
      tags: [history]
      summary: List frozen snapshots
      parameters: [ { $ref: '#/components/parameters/Collection' }, { $ref: '#/components/parameters/Id' } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } }
  /{collection}/{id}/snapshots/{cycle}:
    get:
      tags: [history]
      summary: Fetch one frozen snapshot by cycle
      parameters: [ { $ref: '#/components/parameters/Collection' }, { $ref: '#/components/parameters/Id' }, { name: cycle, in: path, required: true, schema: { type: string } } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } }
  /{collection}/{id}/raw:
    get:
      tags: [history]
      summary: Raw mirrored upstream snapshot
      parameters: [ { $ref: '#/components/parameters/Collection' }, { $ref: '#/components/parameters/Id' } ]
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } }
  /{collection}/{id}/assessments:
    get:
      tags: [assessments]
      summary: Test scores — EDFacts proficiency (reading/math) by cycle, grade and subgroup
      description: >
        Assessment facts for a school, newest cycle first. Percentages are EDFacts privacy-banded
        midpoints (`value`) with the published range in `value_text` and the valid-test N in
        `denominator`; a suppressed cell carries a null `value` and a `value_text` of 'suppressed'.
        Also aliased as `/{collection}/{id}/scores`. Add `?cycle=YYYY-YYYY` to pin a school year.
      parameters:
        - { $ref: '#/components/parameters/Collection' }
        - { $ref: '#/components/parameters/Id' }
        - { name: cycle, in: query, required: false, schema: { type: string }, description: School year, e.g. 2017-2018 }
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } }
  /{collection}/{id}/compare:
    get:
      tags: [history]
      summary: Deep diff between two cycles
      parameters:
        - { $ref: '#/components/parameters/Collection' }
        - { $ref: '#/components/parameters/Id' }
        - { name: from, in: query, required: true, schema: { type: string } }
        - { name: to, in: query, required: true, schema: { type: string } }
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } }
  /admin/requestors:
    get:
      tags: [admin]
      security: [ { AdminKey: [] } ]
      summary: Recent and top requestors
      responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } } }
  /admin/ingest:
    post:
      tags: [admin]
      security: [ { AdminKey: [] } ]
      summary: Trigger ingest
      responses: { '200': { description: OK } }
  /admin/assess:
    post:
      tags: [admin]
      security: [ { AdminKey: [] } ]
      summary: Trigger EDFacts assessment (test-score) ingest for a state+year
      description: 'Body: { state | fips, year, grades?, mode: "run" | "enqueue" }. Resolves each score row''s NCES id to an existing MCI (never mints) and upserts facts into the assess shard.'
      requestBody: { required: true, content: { application/json: { schema: { type: object } } } }
      responses: { '200': { description: OK } }
  /admin/overrides:
    get:
      tags: [admin]
      security: [ { AdminKey: [] } ]
      summary: List active overrides
      responses: { '200': { description: OK } }
    post:
      tags: [admin]
      security: [ { AdminKey: [] } ]
      summary: Create or update an override
      requestBody: { required: true, content: { application/json: { schema: { type: object } } } }
      responses: { '200': { description: OK } }
  /admin/review:
    get:
      tags: [admin]
      security: [ { AdminKey: [] } ]
      summary: Relationship review queue
      responses: { '200': { description: OK } }
    post:
      tags: [admin]
      security: [ { AdminKey: [] } ]
      summary: Act on a review item
      requestBody: { required: true, content: { application/json: { schema: { type: object } } } }
      responses: { '200': { description: OK } }
  /admin/prune:
    post:
      tags: [admin]
      security: [ { AdminKey: [] } ]
      summary: Prune cached/indexed records
      responses: { '200': { description: OK } }
  /admin/backfill/branches:
    post:
      tags: [admin]
      security: [ { AdminKey: [] } ]
      summary: Backfill branch relationships
      responses: { '200': { description: OK } }
components:
  securitySchemes:
    AdminKey:
      type: apiKey
      in: header
      name: X-Admin-Key
  parameters:
    Collection: { name: collection, in: path, required: true, schema: { type: string, enum: [school, district, college, agency, system] } }
    Id: { name: id, in: path, required: true, schema: { type: string } }
  schemas:
    EntityType:
      type: string
      enum: [school-public, school-private, district, postsecondary, system, state-agency]
    Envelope:
      type: object
      required: [request_status, message, data, meta]
      properties:
        request_status: { type: string, enum: [success, error] }
        message: { type: string }
        error_code: { type: string }
        data: {}
        meta:
          type: object
          properties:
            cache: { type: object, properties: { status: { type: string }, layer: { type: string } } }
            upstream: { type: object, properties: { status: { type: string } } }
            sources: { type: array, items: { type: string } }
            cycle: { type: [string, 'null'] }
            as_of: { type: [string, 'null'] }
            response_time_ms: { type: number }
    SearchResult:
      type: object
      properties:
        mci: { type: string, examples: [psed-3sq1uczfowku] }
        entity_type: { $ref: '#/components/schemas/EntityType' }
        name: { type: string }
        city: { type: [string, 'null'] }
        state: { type: [string, 'null'] }
        status: { type: string }
        lat: { type: [number, 'null'] }
        lon: { type: [number, 'null'] }
        distance_km: { type: [number, 'null'], description: Present for /nearest and /nearby results }
        matched_domain: { type: [string, 'null'], description: 'For a domain lookup: the website domain this institution is linked by' }
        via: { type: [string, 'null'], enum: [domain, parent, child, 'null'], description: 'For a domain lookup: "domain" = the institution owns the website; "parent" = surfaced via the governance graph (the district/system the domain-owner rolls up to); "child" = a member school/campus surfaced under a matched district/system domain' }
        relation_type: { type: [string, 'null'], description: 'When via=parent/child: the governance edge (operates = district→school, system_member = system→college, branch_of = parent→branch campus, network_member, governs)' }
        related_to: { type: [string, 'null'], description: 'When via=parent: the MCI of the direct domain match this entity is the parent of. When via=child: the MCI of the matched district/system this entity is a member of.' }
    PoiAddress:
      type: object
      required: [address1, address2, locality, region, postal, country_code, formatted]
      properties:
        address1: { type: [string, 'null'] }
        address2: { type: [string, 'null'] }
        locality: { type: [string, 'null'] }
        region: { type: [string, 'null'] }
        postal: { type: [string, 'null'] }
        country_code: { type: string, minLength: 2, maxLength: 2 }
        formatted: { type: [string, 'null'] }
    PoiSearchResult:
      allOf:
        - { $ref: '#/components/schemas/SearchResult' }
        - type: object
          required: [id, poi_id, name, category, subtype, address, lat, lon, bbox, source, provider, score, detail_url, provenance]
          properties:
            id: { type: string, description: Permanent CCA MCI }
            poi_id: { type: string, description: Compatibility alias of id; permanent CCA MCI }
            category: { type: string, enum: [school, university] }
            subtype: { type: [string, 'null'], description: public/private for K-12; source level such as 4-year for postsecondary }
            address: { $ref: '#/components/schemas/PoiAddress' }
            lat: { type: number }
            lon: { type: number }
            bbox:
              type: ['array', 'null']
              minItems: 4
              maxItems: 4
              items: { type: number }
              description: '[west, south, east, north]; null because cca-schools publishes institution points, not guessed footprints'
            source: { type: string, enum: [cca-schools] }
            provider: { type: string, enum: [CCA Schools] }
            score: { type: number, minimum: 0, maximum: 1 }
            detail_url: { type: [string, 'null'], description: Relative canonical cca-schools API reference }
            provenance:
              type: object
              properties:
                source: { type: string }
                provider: { type: string }
                record_id: { type: string }
                authority: { type: [string, 'null'] }
                country_code: { type: string }
                datasets: { type: array, items: { type: string } }
                cycle: { type: [string, 'null'] }
                coordinate_sources: { type: array, items: { type: string } }
                coordinate_cycle: { type: [string, 'null'] }
                identifiers: { type: array, items: { $ref: '#/components/schemas/Identifier' } }
    Identifier:
      type: object
      properties:
        scheme: { type: string }
        value: { type: string }
        link_confidence: { type: string }
        link_source: { type: string }
        first_seen: { type: string }
        last_seen: { type: string }
    CanonicalAddress:
      type: [object, 'null']
      description: Source-form address components plus a conservative punctuation/case-normalized key for cross-service matching.
      properties:
        full: { type: [string, 'null'], description: Human-readable source address assembled from the mirrored directory fields }
        normalized_full: { type: [string, 'null'], description: Lowercase ASCII alphanumeric matching key; does not guess or expand street abbreviations }
        street: { type: [string, 'null'] }
        city: { type: [string, 'null'] }
        state: { type: [string, 'null'] }
        zip: { type: [string, 'null'], description: Compatibility alias of postal_code }
        postal_code: { type: [string, 'null'] }
        country: { type: string }
    InstitutionPoint:
      type: object
      required: [lat, lon, precision, source]
      description: >
        Validated map point. `source-reported` means the upstream institution directory supplied the
        coordinate; it is deliberately not described as rooftop. `cca-geocode` points are accepted only
        at rooftop/interpolated/street precision. Postal and place centroids never appear here.
      properties:
        lat: { type: number }
        lon: { type: number }
        method: { type: string, enum: [source, cca-geocode] }
        precision: { type: string, enum: [source-reported, rooftop, interpolated, street] }
        approximate: { type: [boolean, 'null'], description: null when the upstream source did not publish an accuracy class }
        source: { type: string, description: Primary dataset/source that supplied the point }
        sources: { type: array, items: { type: string } }
        cycle: { type: [string, 'null'], description: Snapshot cycle that supplied the source coordinate when known }
        score: { type: [number, 'null'], description: cca-geocode match score when method=cca-geocode }
        match_source: { type: [string, 'null'], description: Native address dataset selected by cca-geocode }
        refined_by: { type: [string, 'null'], description: Corroborating source when cca-geocode refined the native match }
    NearbyResult:
      allOf:
        - { $ref: '#/components/schemas/SearchResult' }
        - type: object
          required: [mci, entity_type, name, lat, lon, distance_km, point, source_identifiers]
          properties:
            authority: { type: string }
            country: { type: string }
            postal_code: { type: [string, 'null'] }
            address: { $ref: '#/components/schemas/CanonicalAddress' }
            identifiers:
              type: array
              description: Compatibility alias of source_identifiers for consumers using the fleet identifier-array convention.
              items: { $ref: '#/components/schemas/Identifier' }
            source_identifiers:
              type: array
              description: Active, source-backed aliases from the identifier graph. The derived website-domain alias is excluded. Do not use these as the permanent primary key.
              items: { $ref: '#/components/schemas/Identifier' }
            ein:
              type: string
              description: Present only when an upstream source supplied an active EIN identifier; never inferred or fabricated.
            point: { $ref: '#/components/schemas/InstitutionPoint' }
    SearchCenter:
      type: object
      description: Geocoded center echoed by /nearby. This may be an explicitly approximate postal/place centroid.
      properties:
        lat: { type: number }
        lon: { type: number }
        city: { type: [string, 'null'] }
        state: { type: [string, 'null'] }
        postal_code: { type: [string, 'null'] }
        kind: { type: string }
        precision: { type: string }
        approximate: { type: boolean }
        label: { type: [string, 'null'] }
        source: { type: string }
        match: { type: [object, 'null'] }
        matched: { type: [object, 'null'] }
        provenance: { type: object }
    NearbyData:
      type: object
      required: [count, center, radius_km, geocoded_count, unmappable_count, geocode_candidate_count, geocode_candidates_truncated, results]
      properties:
        count: { type: integer }
        center: { type: object, required: [lat, lon], properties: { lat: { type: number }, lon: { type: number } } }
        radius_km: { type: number }
        geocoded_count: { type: integer, description: Returned institutions whose missing source coordinates were filled by a street-level cca-geocode match }
        unmappable_count: { type: integer, description: Considered missing-coordinate records omitted because no street-level point was available }
        geocode_candidate_count: { type: integer, description: Missing-coordinate records matching the supplied ZIP or city/state hint before geocoding }
        geocode_candidates_truncated: { type: boolean, description: True when more than 100 missing-coordinate candidates matched the hint; the fallback supplement is then non-exhaustive }
        results: { type: array, items: { $ref: '#/components/schemas/NearbyResult' } }
        geocode: { $ref: '#/components/schemas/SearchCenter' }
    NearbyEnvelope:
      allOf:
        - { $ref: '#/components/schemas/Envelope' }
        - type: object
          properties:
            data: { $ref: '#/components/schemas/NearbyData' }
    EntityBundle:
      type: object
      properties:
        mci: { type: string }
        entity_type: { $ref: '#/components/schemas/EntityType' }
        authority: { type: string }
        country: { type: string }
        name: { type: string }
        status: { type: string }
        cycle: { type: string }
        first_cycle: { type: string }
        last_cycle: { type: string }
        reporting_unit: { type: boolean }
        location: { type: object, properties: { city: { type: [string, 'null'] }, state: { type: [string, 'null'] }, zip: { type: [string, 'null'] }, lat: { type: [number, 'null'] }, lon: { type: [number, 'null'] } } }
        identifiers: { type: object, additionalProperties: { type: string } }
        identifiers_detail: { type: array, items: { $ref: '#/components/schemas/Identifier' } }
        directory: { type: object }
        characteristics: { type: object }
        codes: { type: object }
        source: { type: object }
        relationships: { type: object }
        rollup: { type: object }
        state_exams: { type: array, description: Applicable state assessment programs for this school/district, items: { $ref: '#/components/schemas/ExamProgram' } }
        ny_studed: { type: object, description: 'New York (NYSED STUDED) school-level discipline + class size: out-of-school suspensions (count + rate) and average class size with a core-subject breakdown, plus a multi-year trend. NY public schools only.' }
        tx_staar: { type: object, description: 'Texas (TEA STAAR) campus-level Reading-ELA/Math/Science/Social-Studies performance at the three STAAR levels (Approaches/Meets/Masters Grade Level; Meets is the proficiency bar), merging grades 3-8 with End-of-Course exams, plus a multi-year Meets trend. TX public schools only.' }
        nc: { type: object, description: 'North Carolina (NC DPI EOG/EOC) school-level ELA/Math/Science proficiency as GLP (Grade Level Proficient, Levels 3-5; the headline bar) and CCR (College & Career Ready, Levels 4-5), merging grades 3-8 with End-of-Course exams, plus a multi-year GLP trend. NC public schools only.' }
        wi: { type: object, description: 'Wisconsin (DPI Forward Exam) school-level ELA/Math/Science/Social Studies proficiency as the share Meeting or Advanced (the headline proficiency bar), aggregated across tested grades, plus a multi-year trend. WI public schools only.' }
        in: { type: object, description: 'Indiana (IDOE ILEARN) school-level ELA/Math/Science/Social Studies proficiency as the share At or Above Proficiency (the headline bar), using the published School Total across tested grades, plus a multi-year trend. IN public schools only.' }
        il: { type: object, description: 'Illinois (ISBE Report Card) school-level ELA/Math/Science overall % Proficiency — blending the IAR (grades 3-8), the SAT (grade 11) and the ISA (science) — keyed on the RCDTS-derived school id, plus a multi-year trend. IL public schools only.' }
    ExamProgram:
      type: object
      description: One curated state (or federal) assessment program from the exam catalog.
      properties:
        id: { type: string, examples: [ny-regents] }
        authority: { type: string, examples: [nysed] }
        state: { type: [string, 'null'], description: USPS code; null for federal programs }
        fips: { type: [integer, 'null'] }
        name: { type: string, examples: ['New York State Regents Examinations'] }
        short: { type: string, examples: [Regents] }
        vendor: { type: [string, 'null'] }
        type: { type: string, enum: [grades-3-8, comprehensive, high-school-eoc, high-school-general, high-school-exit, college-entrance, science] }
        type_label: { type: string }
        level: { type: string, enum: [elementary-middle, high, all] }
        level_label: { type: string }
        grades: { type: [string, 'null'], examples: ['9-12'] }
        subjects: { type: array, items: { type: string } }
        subject_labels: { type: array, items: { type: string } }
        proficiency: { type: [string, 'null'], description: How the proficiency/passing bar is defined }
        url: { type: [string, 'null'] }
        mirrored: { type: boolean, description: True when CCA ingests this program's school-level results }
        dataset: { type: [string, 'null'], description: Backing dataset id when mirrored }
        bundle_key: { type: [string, 'null'], description: Key under which scores attach to a school bundle }
        cadence: { type: [string, 'null'] }
        notes: { type: [string, 'null'] }
        retired: { type: boolean, description: True for a historical program a state no longer administers }
        years: { type: [string, 'null'], description: 'Administration span for a retired program (e.g. 1998–2010)' }
        replaced_by: { type: [string, 'null'], description: Short name of the successor program that replaced a retired one }
        status: { type: string, enum: [mirrored, current, retired], description: 'Lifecycle: mirrored (we ingest scores), current (active, not mirrored), or retired' }
    Status:
      type: object
      properties:
        service: { type: string }
        status: { type: string }
        bindings: { type: object }
        coverage:
          type: object
          properties:
            institutions: { type: integer }
            by_type: { type: object, additionalProperties: { type: integer } }
            states_covered: { type: integer }
            identifiers: { type: integer }
            identifier_schemes:
              type: array
              items:
                type: object
                properties:
                  scheme: { type: string }
                  count: { type: integer }
            relationships: { type: object }
            relations_pending_review: { type: integer }
        history: { type: object, properties: { change_events: { type: integer }, last_change_at: { type: string } } }
        authorities: { type: integer }
        datasets: { type: array, items: { type: object } }
        generated_at: { type: string }
