openapi: 3.1.0
info:
  title: Zapex API
  version: '1'
  summary: The address and location layer for UK organisations.
  description: |
    Address autocomplete, lookup and cleansing; forward and reverse geocoding, the areas of
    a point, zones and places of interest; hosted location lists; and maps.

    Every route takes an API key, in `Authorization: Bearer <key>`. A **secret key**
    (`zpx_live_…`) is for a server, and a **publishable key** (`zpx_pk_live_…`) for a page:
    it may call only what a browser needs, from the origins it names. A **test key**
    (`zpx_test_…`, `zpx_pk_test_…`) calls the sandbox: the same routes over fixture data,
    with no credits used. The sandbox's addresses are those of the postcodes SA1 1DF,
    SA1 1RE, SA1 1RP, SA1 1TY and SA1 8JQ, in Swansea.

    Every failure answers with a problem details body (RFC 9457, `application/problem+json`):
    `type`, `title`, `status` and `detail`, with the API's `code` and, where the failure is
    of one field, `field`. See the codes of each route and https://zapex.co.uk/docs/errors.
  contact:
    email: contact@zapex.co.uk
servers:
  - url: https://api.zapex.co.uk
security:
  - bearer: []
tags:
  - name: Address
  - name: Geocoding
  - name: Cleansing
  - name: Place
  - name: Locations
  - name: Maps
  - name: Datasets

paths:
  /v1/addresses:
    get:
      tags: [Address]
      summary: Suggest addresses as the customer types
      description: |
        Narrows to an address from the starts of its words, its number and its outward code,
        in any order (`ches 37 brom mk43`), forgiving a typo. Scope `address:search`. Free:
        the lookup of the pick is charged.
      operationId: searchAddresses
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, maxLength: 100 } }
      responses:
        '200':
          description: At most 10 suggestions, the best first.
          content:
            application/json:
              schema:
                type: object
                required: [query, suggestions]
                properties:
                  query: { type: string }
                  suggestions:
                    type: array
                    items: { $ref: '#/components/schemas/Suggestion' }
        default: { $ref: '#/components/responses/Error' }
  /v1/addresses/{uprn}:
    get:
      tags: [Address]
      summary: Look up an address by its UPRN
      description: Scope `address:lookup`. One credit where the address is found.
      operationId: getAddress
      parameters:
        - {
            name: uprn,
            in: path,
            required: true,
            schema: { type: string, pattern: '^[0-9]{1,12}$' }
          }
      responses:
        '200':
          description: The address's record.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AddressRecord' }
        '404': { $ref: '#/components/responses/Error' }
        default: { $ref: '#/components/responses/Error' }
  /v1/postcodes/{postcode}/addresses:
    get:
      tags: [Address]
      summary: Every address of a postcode
      description: Scope `address:lookup`. One credit where the postcode has addresses.
      operationId: getPostcodeAddresses
      parameters:
        - { name: postcode, in: path, required: true, schema: { type: string }, example: SA1 1RE }
      responses:
        '200':
          description: The postcode as Royal Mail writes it, and its addresses in order.
          content:
            application/json:
              schema:
                type: object
                required: [postcode, addresses]
                properties:
                  postcode: { type: string }
                  addresses: { type: array, items: { $ref: '#/components/schemas/AddressRecord' } }
        '404': { $ref: '#/components/responses/Error' }
        default: { $ref: '#/components/responses/Error' }

  /v1/geocode/forward:
    get:
      tags: [Geocoding]
      summary: Geocode an address as written
      description: Scope `geocode`. One credit.
      operationId: forwardGeocode
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, maxLength: 200 } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 10, default: 1 } }
      responses:
        '200':
          description: The addresses that the text may be, the best first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results: { type: array, items: { $ref: '#/components/schemas/ForwardMatch' } }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [Geocoding]
      summary: Geocode up to 100 addresses
      description: Scope `geocode`. One credit each.
      operationId: forwardGeocodeBatch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [queries]
              properties:
                queries:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    type: object
                    required: [q]
                    properties:
                      q: { type: string, maxLength: 200 }
                      limit: { type: integer, minimum: 1, maximum: 10 }
      responses:
        '200':
          description: What each query found, in their order.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        results:
                          { type: array, items: { $ref: '#/components/schemas/ForwardMatch' } }
        default: { $ref: '#/components/responses/Error' }
  /v1/geocode/reverse:
    get:
      tags: [Geocoding]
      summary: The addresses nearest a point
      description: Scope `geocode`. One credit.
      operationId: reverseGeocode
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number } }
        - { name: lng, in: query, required: true, schema: { type: number } }
        - {
            name: radius,
            in: query,
            schema: { type: number, maximum: 500, default: 50 },
            description: Metres.
          }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 10, default: 1 } }
        - {
            name: nearest_point,
            in: query,
            schema: { type: boolean, default: false },
            description: Every address at the nearest point,
            as the flats of a block,
            at most 100.
          }
      responses:
        '200':
          description: The addresses within the radius, nearest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results: { type: array, items: { $ref: '#/components/schemas/ReverseMatch' } }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [Geocoding]
      summary: Reverse geocode up to 100 points
      description: Scope `geocode`. One credit each.
      operationId: reverseGeocodeBatch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [queries]
              properties:
                queries:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    type: object
                    required: [lat, lng]
                    properties:
                      lat: { type: number }
                      lng: { type: number }
                      radius: { type: number }
                      limit: { type: integer }
                      nearest_point: { type: boolean }
      responses:
        '200':
          description: What each query found, in their order.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        results:
                          { type: array, items: { $ref: '#/components/schemas/ReverseMatch' } }
        default: { $ref: '#/components/responses/Error' }

  /v1/cleansing/jobs:
    post:
      tags: [Cleansing]
      summary: Send a job of addresses to cleanse
      description: |
        Up to 100,000 addresses as written, each matched to its record apart from this
        request. Scope `geocode`. One credit each, as each is cleansed; a live job needs the
        credits it will use (`credits_exhausted`).
      operationId: createCleansingJob
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 100000
                  items:
                    type: object
                    required: [address]
                    properties:
                      id:
                        {
                          type: string,
                          maxLength: 100,
                          description: Your ID of the address,
                          given back with its result.
                        }
                      address: { type: string, maxLength: 500 }
      responses:
        '202':
          description: The job, to poll at its `Location`.
          headers:
            Location: { schema: { type: string } }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CleansingJob' }
        default: { $ref: '#/components/responses/Error' }
  /v1/cleansing/jobs/{id}:
    get:
      tags: [Cleansing]
      summary: A job, and how far through it is
      operationId: getCleansingJob
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: The job.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CleansingJob' }
        '404': { $ref: '#/components/responses/Error' }
  /v1/cleansing/jobs/{id}/results:
    get:
      tags: [Cleansing]
      summary: A page of a job's results
      operationId: getCleansingResults
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - {
            name: after,
            in: query,
            schema: { type: integer, minimum: 0, default: 0 },
            description: The position to start from.
          }
        - {
            name: limit,
            in: query,
            schema: { type: integer, minimum: 1, maximum: 1000, default: 100 }
          }
      responses:
        '200':
          description: The job, its items from `after`, and the `after` of the next page.
          content:
            application/json:
              schema:
                type: object
                properties:
                  job: { $ref: '#/components/schemas/CleansingJob' }
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        position: { type: integer }
                        id: { type: [string, 'null'] }
                        address: { type: string }
                        status:
                          { type: string, enum: [pending, matched, located, uncertain, unmatched] }
                        match:
                          oneOf:
                            - { $ref: '#/components/schemas/ForwardMatch' }
                            - { type: 'null' }
                  next: { type: [integer, 'null'] }
        '404': { $ref: '#/components/responses/Error' }

  /v1/geography:
    get:
      tags: [Place]
      summary: The areas of a point or a postcode
      description: |
        Give a point (`lat`, `lng`), for the areas whose boundaries cover it, or a
        `postcode`, for those the ONS Postcode Directory gives it. Scope `place:read`. One
        credit.
      operationId: getGeography
      parameters:
        - { name: lat, in: query, schema: { type: number } }
        - { name: lng, in: query, schema: { type: number } }
        - { name: postcode, in: query, schema: { type: string } }
      responses:
        '200':
          description: The areas, by kind.
          content:
            application/json:
              schema:
                type: object
                properties:
                  source: { type: string, enum: [boundaries, postcode] }
                  postcode: { type: string }
                  areas: { $ref: '#/components/schemas/Geography' }
        '404': { $ref: '#/components/responses/Error' }
        default: { $ref: '#/components/responses/Error' }
  /v1/zones/contains:
    post:
      tags: [Place]
      summary: Which of your zones hold each point
      description: Scope `place:read`. One credit for each point.
      operationId: zonesContain
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [zones, points]
              properties:
                zones:
                  type: object
                  description: A GeoJSON FeatureCollection of Polygons and MultiPolygons, at most 100, with 100,000 positions in all. A zone's ID is its Feature's `id`, or its property `id`.
                points:
                  type: array
                  maxItems: 1000
                  items:
                    type: object
                    required: [id, lat, lng]
                    properties:
                      id: { type: string }
                      lat: { type: number }
                      lng: { type: number }
      responses:
        '200':
          description: Each point, and the IDs of the zones that hold it.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        zones: { type: array, items: { type: string } }
        default: { $ref: '#/components/responses/Error' }
  /v1/places:
    get:
      tags: [Place]
      summary: Places of interest near a point
      description: Rail stations, Tube, metro and tram stations and bus stops (Great Britain), and schools with their Ofsted inspection (England). Scope `place:read`. One credit.
      operationId: listPlaces
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number } }
        - { name: lng, in: query, required: true, schema: { type: number } }
        - {
            name: categories,
            in: query,
            schema: { type: string },
            description: 'A comma list of rail_station, metro_station, bus_stop and school. Every one by default.'
          }
        - { name: radius_m, in: query, schema: { type: number, maximum: 10000, default: 1000 } }
        - {
            name: limit,
            in: query,
            schema: { type: integer, minimum: 1, maximum: 100, default: 10 }
          }
      responses:
        '200':
          description: The places, nearest first.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Places' }
        default: { $ref: '#/components/responses/Error' }
  /v1/places/amenities:
    get:
      tags: [Place]
      summary: The nearest place of each category
      description: Scope `place:read`. One credit.
      operationId: getAmenities
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number } }
        - { name: lng, in: query, required: true, schema: { type: number } }
        - { name: categories, in: query, schema: { type: string } }
        - {
            name: max_distance_m,
            in: query,
            schema: { type: number, maximum: 50000, default: 10000 }
          }
      responses:
        '200':
          description: The nearest of each category within the distance, in the order of the categories.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Places' }
        default: { $ref: '#/components/responses/Error' }

  /v1/location-lists:
    get:
      tags: [Locations]
      summary: Your location lists
      description: Scope `locations:read`.
      operationId: listLocationLists
      responses:
        '200':
          description: The lists.
          content:
            application/json:
              schema:
                type: object
                properties:
                  lists: { type: array, items: { $ref: '#/components/schemas/LocationList' } }
    post:
      tags: [Locations]
      summary: Make a location list
      description: Scope `locations:write`.
      operationId: createLocationList
      requestBody:
        required: true
        content:
          application/json:
            schema:
              {
                type: object,
                required: [name],
                properties: { name: { type: string, maxLength: 100 } }
              }
      responses:
        '201':
          description: The list.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LocationList' }
        default: { $ref: '#/components/responses/Error' }
  /v1/location-lists/{list_id}:
    parameters:
      - { name: list_id, in: path, required: true, schema: { type: string } }
    get:
      tags: [Locations]
      summary: A list
      operationId: getLocationList
      responses:
        '200':
          {
            description: The list.,
            content: { application/json: { schema: { $ref: '#/components/schemas/LocationList' } } }
          }
        '404': { $ref: '#/components/responses/Error' }
    patch:
      tags: [Locations]
      summary: Rename a list
      operationId: renameLocationList
      requestBody:
        required: true
        content:
          application/merge-patch+json:
            schema: { type: object, required: [name], properties: { name: { type: string } } }
      responses:
        '200':
          {
            description: The list.,
            content: { application/json: { schema: { $ref: '#/components/schemas/LocationList' } } }
          }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [Locations]
      summary: Delete a list and its locations
      operationId: deleteLocationList
      responses:
        '204': { description: Deleted. }
        '404': { $ref: '#/components/responses/Error' }
  /v1/location-lists/{list_id}/locations:
    get:
      tags: [Locations]
      summary: A page of a list's locations
      description: Scope `locations:read`.
      operationId: listLocations
      parameters:
        - { name: list_id, in: path, required: true, schema: { type: string } }
        - { name: cursor, in: query, schema: { type: string } }
        - {
            name: limit,
            in: query,
            schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
          }
      responses:
        '200':
          description: The locations, and the cursor of the next page.
          content:
            application/json:
              schema:
                type: object
                properties:
                  locations: { type: array, items: { $ref: '#/components/schemas/Location' } }
                  next_cursor: { type: [string, 'null'] }
    post:
      tags: [Locations]
      summary: Write up to 1,000 locations
      description: Each item has its own result; the batch is `200` unless the list is not found or it has too many items. Scope `locations:write`.
      operationId: batchUpsertLocations
      parameters:
        - { name: list_id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [locations]
              properties:
                locations:
                  type: array
                  maxItems: 1000
                  items:
                    allOf:
                      - { $ref: '#/components/schemas/LocationInput' }
                      - { type: object, required: [id], properties: { id: { type: string } } }
      responses:
        '200':
          description: A result for each item, in their order.
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        index: { type: integer }
                        id: { type: string }
                        outcome: { type: string, enum: [created, updated, unchanged, failed] }
                        location: { $ref: '#/components/schemas/Location' }
                        error: { $ref: '#/components/schemas/ItemError' }
        default: { $ref: '#/components/responses/Error' }
  /v1/location-lists/{list_id}/locations/{id}:
    parameters:
      - { name: list_id, in: path, required: true, schema: { type: string } }
      - {
          name: id,
          in: path,
          required: true,
          schema: { type: string, pattern: '^[A-Za-z0-9._:-]{1,64}$' },
          description: Your ID of the location.
        }
    get:
      tags: [Locations]
      summary: A location
      operationId: getLocation
      responses:
        '200':
          {
            description: The location.,
            content: { application/json: { schema: { $ref: '#/components/schemas/Location' } } }
          }
        '404': { $ref: '#/components/responses/Error' }
    put:
      tags: [Locations]
      summary: Write a location in full
      description: An address without `lat` and `lng` is geocoded, for one credit. Scope `locations:write`.
      operationId: putLocation
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LocationInput' }
      responses:
        '200':
          {
            description: Updated,
            or unchanged.,
            content:
              { application/json: { schema: { $ref: '#/components/schemas/WrittenLocation' } } }
          }
        '201':
          {
            description: Created.,
            content:
              { application/json: { schema: { $ref: '#/components/schemas/WrittenLocation' } } }
          }
        default: { $ref: '#/components/responses/Error' }
    patch:
      tags: [Locations]
      summary: Change some of a location's fields
      description: A field given as `null` is removed.
      operationId: patchLocation
      requestBody:
        required: true
        content:
          application/merge-patch+json:
            schema: { $ref: '#/components/schemas/LocationInput' }
      responses:
        '200':
          {
            description: The location.,
            content:
              { application/json: { schema: { $ref: '#/components/schemas/WrittenLocation' } } }
          }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [Locations]
      summary: Delete a location
      operationId: deleteLocation
      responses:
        '204': { description: Deleted. }
        '404': { $ref: '#/components/responses/Error' }
  /v1/location-lists/{list_id}/changes:
    get:
      tags: [Locations]
      summary: A list's changes since a sequence
      operationId: listChanges
      parameters:
        - { name: list_id, in: path, required: true, schema: { type: string } }
        - { name: since, in: query, schema: { type: integer, minimum: 0, default: 0 } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 500 } }
      responses:
        '200':
          description: The changes in order, the cursor of the next page, and the list's sequence.
          content:
            application/json:
              schema:
                type: object
                properties:
                  changes:
                    type: array
                    items:
                      type: object
                      properties:
                        seq: { type: integer }
                        id: { type: string }
                        change: { type: string, enum: [upserted, deleted] }
                        location: { $ref: '#/components/schemas/Location' }
                        deleted_at: { type: [string, 'null'], format: date-time }
                  next_cursor: { type: [integer, 'null'] }
                  list_seq: { type: integer }
  /v1/location-lists/{list_id}/nearest:
    get:
      tags: [Locations]
      summary: The nearest of a list's locations
      description: |
        From a point, a postcode or an address (one credit more), within a radius or a box.
        Scope `locations:query`, which a publishable key may have. One credit.
      operationId: nearestLocations
      parameters:
        - { name: list_id, in: path, required: true, schema: { type: string } }
        - { name: lat, in: query, schema: { type: number } }
        - { name: lng, in: query, schema: { type: number } }
        - { name: postcode, in: query, schema: { type: string } }
        - { name: address, in: query, schema: { type: string, maxLength: 200 } }
        - { name: radius_m, in: query, schema: { type: number, maximum: 1000000 } }
        - {
            name: bbox,
            in: query,
            schema: { type: string },
            description: '`west,south,east,north`'
          }
        - {
            name: limit,
            in: query,
            schema: { type: integer, minimum: 1, maximum: 100, default: 10 }
          }
        - {
            name: tags,
            in: query,
            schema: { type: string },
            description: A comma list; a location has them all.
          }
        - {
            name: status,
            in: query,
            schema: { type: string, enum: [active, inactive, all], default: active }
          }
        - { name: open_now, in: query, schema: { type: boolean } }
      responses:
        '200':
          description: The origin as it was found, and the locations, nearest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  origin: { $ref: '#/components/schemas/Origin' }
                  results:
                    type: array
                    items:
                      allOf:
                        - { $ref: '#/components/schemas/Location' }
                        - type: object
                          properties:
                            distance_m: { type: [number, 'null'] }
                            open_now: { type: [boolean, 'null'] }
        default: { $ref: '#/components/responses/Error' }
  /v1/location-lists/{list_id}/search:
    get:
      tags: [Locations]
      summary: Search a list by name or ID
      operationId: searchLocations
      parameters:
        - { name: list_id, in: path, required: true, schema: { type: string } }
        - { name: q, in: query, required: true, schema: { type: string, maxLength: 100 } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100 } }
        - { name: tags, in: query, schema: { type: string } }
        - { name: status, in: query, schema: { type: string } }
        - { name: open_now, in: query, schema: { type: boolean } }
      responses:
        '200':
          description: The matches, the best first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  query: { type: string }
                  results: { type: array, items: { $ref: '#/components/schemas/Location' } }
        default: { $ref: '#/components/responses/Error' }
  /v1/locations/nearest:
    post:
      tags: [Locations]
      summary: The nearest of up to 1,000 points sent with the query
      operationId: nearestInline
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [locations]
              properties:
                lat: { type: number }
                lng: { type: number }
                postcode: { type: string }
                address: { type: string }
                radius_m: { type: number }
                bbox: { type: array, items: { type: number }, minItems: 4, maxItems: 4 }
                limit: { type: integer }
                locations:
                  type: array
                  maxItems: 1000
                  items:
                    type: object
                    required: [id, lat, lng]
                    properties:
                      id: { type: string }
                      lat: { type: number }
                      lng: { type: number }
      responses:
        '200':
          description: The origin, and the points nearest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  origin: { $ref: '#/components/schemas/Origin' }
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        lat: { type: number }
                        lng: { type: number }
                        distance_m: { type: [number, 'null'] }
        default: { $ref: '#/components/responses/Error' }

  /v1/maps/styles/{style}:
    get:
      tags: [Maps]
      summary: A map's style
      description: |
        For MapLibre GL: `new maplibregl.Map({style: 'https://api.zapex.co.uk/v1/maps/styles/color?key=zpx_pk_live_…'})`.
        Its parts are at the routes below, with the publishable key in their URLs. Scope
        `maps:read`. One credit: a map load.
      operationId: getStyle
      security: [{ bearer: [] }, { key: [] }]
      parameters:
        - { name: style, in: path, required: true, schema: { type: string, enum: [color] } }
      responses:
        '200':
          {
            description: A MapLibre style.,
            content: { application/json: { schema: { type: object } } }
          }
  /v1/maps/tilesets/{archive}:
    get:
      tags: [Maps]
      summary: The TileJSON of an archive
      operationId: getTileset
      security: [{ bearer: [] }, { key: [] }]
      parameters:
        - {
            name: archive,
            in: path,
            required: true,
            schema: { type: string, enum: [basemap, terrain, landcover] }
          }
      responses:
        '200':
          {
            description: TileJSON 3.0.0.,
            content: { application/json: { schema: { type: object } } }
          }
  /v1/maps/tiles/{archive}/{z}/{x}/{y}:
    get:
      tags: [Maps]
      summary: A tile
      description: As it is stored, with its `Content-Encoding`. Not rate limited, and not charged.
      operationId: getTile
      security: [{ bearer: [] }, { key: [] }]
      parameters:
        - { name: archive, in: path, required: true, schema: { type: string } }
        - { name: z, in: path, required: true, schema: { type: integer } }
        - { name: x, in: path, required: true, schema: { type: integer } }
        - {
            name: y,
            in: path,
            required: true,
            schema: { type: string },
            description: The row,
            with or without `.pbf`.
          }
      responses:
        '200': { description: The tile., content: { application/vnd.mapbox-vector-tile: {} } }
        '204': { description: The archive has no tile there. }
  /v1/maps/fonts/{fontstack}/{range}:
    get:
      tags: [Maps]
      summary: A range of a font stack's glyphs
      operationId: getGlyphs
      security: [{ bearer: [] }, { key: [] }]
      parameters:
        - { name: fontstack, in: path, required: true, schema: { type: string } }
        - { name: range, in: path, required: true, schema: { type: string }, example: 0-255.pbf }
      responses:
        '200': { description: The glyphs., content: { application/x-protobuf: {} } }
  /v1/maps/sprites/{style}/{file}:
    get:
      tags: [Maps]
      summary: A file of a style's sprite
      operationId: getSprite
      security: [{ bearer: [] }, { key: [] }]
      parameters:
        - { name: style, in: path, required: true, schema: { type: string } }
        - {
            name: file,
            in: path,
            required: true,
            schema: { type: string, enum: [sprite.json, sprite.png, sprite@2x.json, sprite@2x.png] }
          }
      responses:
        '200': { description: The file. }
  /v1/maps/static:
    get:
      tags: [Maps]
      summary: A static map, as a PNG
      description: Scope `maps:read`. One credit.
      operationId: getStaticMap
      security: [{ bearer: [] }, { key: [] }]
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number } }
        - { name: lng, in: query, required: true, schema: { type: number } }
        - {
            name: zoom,
            in: query,
            required: true,
            schema: { type: number, minimum: 0, maximum: 20 }
          }
        - {
            name: width,
            in: query,
            required: true,
            schema: { type: integer, minimum: 1, maximum: 1280 }
          }
        - {
            name: height,
            in: query,
            required: true,
            schema: { type: integer, minimum: 1, maximum: 1280 }
          }
        - { name: scale, in: query, schema: { type: integer, enum: [1, 2], default: 1 } }
        - {
            name: markers,
            in: query,
            schema: { type: string },
            description: 'Up to 50, as `lng,lat;lng,lat`.'
          }
        - { name: style, in: query, schema: { type: string, default: color } }
      responses:
        '200': { description: The map., content: { image/png: {} } }
        default: { $ref: '#/components/responses/Error' }

  /v1/datasets:
    get:
      tags: [Datasets]
      summary: The datasets the API answers from
      description: Each dataset's publisher, licence and coverage, and the release loaded. No key is needed.
      operationId: listDatasets
      security: []
      responses:
        '200':
          description: The datasets.
          content:
            application/json:
              schema:
                type: object
                properties:
                  datasets:
                    type: array
                    items:
                      type: object
                      properties:
                        name: { type: string }
                        title: { type: string }
                        publisher: { type: string }
                        licence: { type: string }
                        coverage: { type: string }
                        release: { type: [string, 'null'] }
                        loaded_at: { type: [string, 'null'], format: date-time }

components:
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: An API key, `zpx_live_…`, `zpx_test_…`, `zpx_pk_live_…` or `zpx_pk_test_…`.
    key:
      type: apiKey
      in: query
      name: key
      description: A publishable key, for the maps only, which a map sends with each part.
  responses:
    Error:
      description: A failure, as problem details (RFC 9457), with its code where it has one.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Error' }
  schemas:
    Error:
      type: object
      description: A problem details object (RFC 9457). `type`, `title`, `status` and `detail` are the standard's; `code`, `field`, `errors` and `scope` are the API's own.
      required: [type, title, status, detail]
      properties:
        type:
          type: string
          format: uri
          description: 'The documentation of the code: `https://zapex.co.uk/docs/errors/{code}`, or `about:blank` for a failure with no code.'
        title:
          {
            type: string,
            description: 'A short phrase for the kind of failure, the same for every occurrence.'
          }
        status: { type: integer, description: The HTTP status of the response. }
        detail:
          {
            type: string,
            description: "What went wrong, for people. For a failure with a code, the domain's own words."
          }
        code:
          type: string
          enum:
            - invalid_request
            - invalid_param
            - geocode_failed
            - list_not_found
            - location_not_found
            - name_taken
            - list_limit_reached
            - out_of_coverage
            - invalid_api_key
            - organisation_suspended
            - insufficient_scope
            - origin_not_allowed
            - secret_key_from_browser
            - rate_limited
            - credits_exhausted
            - monthly_cap_reached
            - insufficient_role
            - cross_site_request
            - already_member
            - last_owner
            - payment_failed
        field:
          { type: string, description: 'The field the failure is of: `lat`, `locations[2].lat`.' }
        errors:
          type: object
          description: "For a request that broke a `validator` rule: each field's failures, by field."
          additionalProperties: true
    ItemError:
      type: object
      description: Why an item of a batch failed, as the error body of a request would say it.
      required: [code, detail]
      properties:
        code: { type: string }
        field: { type: string }
        detail: { type: string }

        scope:
          type: string
          enum: [organisation, key, ip, ip_daily]
          description: 'For `rate_limited`: the limit that refused the request.'
    Suggestion:
      type: object
      properties:
        street_address: { type: string, example: 1b Gloucester Place, Maritime Quarter }
        uprn: { type: string, example: '100100357412' }
        post_town: { type: string, example: SWANSEA }
        postcode: { type: string, example: SA1 1TY }
    Area:
      type: [object, 'null']
      properties:
        code: { type: string, example: W06000011 }
        name: { type: [string, 'null'], example: Swansea }
    Geography:
      type: object
      properties:
        country: { $ref: '#/components/schemas/Area' }
        region: { $ref: '#/components/schemas/Area' }
        county: { $ref: '#/components/schemas/Area' }
        local_authority: { $ref: '#/components/schemas/Area' }
        constituency: { $ref: '#/components/schemas/Area' }
        ward: { $ref: '#/components/schemas/Area' }
        msoa: { $ref: '#/components/schemas/Area' }
        lsoa: { $ref: '#/components/schemas/Area' }
    AddressRecord:
      type: object
      properties:
        uprn: { type: string }
        udprn: { type: integer }
        components:
          type: object
          description: PAF's elements, under their names.
          properties:
            organisation_name: { type: [string, 'null'] }
            department_name: { type: [string, 'null'] }
            po_box: { type: [string, 'null'] }
            sub_building_name: { type: [string, 'null'] }
            building_name: { type: [string, 'null'] }
            building_number: { type: [string, 'null'] }
            dependent_thoroughfare_and_descriptor: { type: [string, 'null'] }
            thoroughfare_and_descriptor: { type: [string, 'null'] }
            double_dependent_locality: { type: [string, 'null'] }
            dependent_locality: { type: [string, 'null'] }
        lines:
          type: object
          description: The address as Royal Mail writes it above the post town.
          properties:
            line_1: { type: string }
            line_2: { type: [string, 'null'] }
            line_3: { type: [string, 'null'] }
        post_town: { type: string }
        postcode: { type: string }
        country: { type: string, description: ISO 3166-1 alpha-2. }
        location:
          type: object
          properties:
            lat: { type: number }
            lng: { type: number }
        geography: { $ref: '#/components/schemas/Geography' }
    ForwardMatch:
      allOf:
        - { $ref: '#/components/schemas/AddressRecord' }
        - type: object
          properties:
            confidence:
              { type: number, description: That this is the address meant. 0.8 or more is sure. }
            location_confidence:
              { type: number, description: That this is where the address meant is. }
            matched:
              type: object
              properties:
                organisation: { $ref: '#/components/schemas/PartMatch' }
                premise: { $ref: '#/components/schemas/PartMatch' }
                thoroughfare: { $ref: '#/components/schemas/PartMatch' }
                locality: { $ref: '#/components/schemas/PartMatch' }
                post_town: { $ref: '#/components/schemas/PartMatch' }
                postcode: { $ref: '#/components/schemas/PartMatch' }
                unmatched_words: { type: array, items: { type: string } }
    PartMatch:
      type: string
      enum: [full, partial, incorrect, missing, not_applicable]
    ReverseMatch:
      allOf:
        - { $ref: '#/components/schemas/AddressRecord' }
        - type: object
          properties:
            distance_m: { type: number }
    CleansingJob:
      type: object
      properties:
        id: { type: string }
        status: { type: string, enum: [queued, running, completed] }
        total: { type: integer }
        done: { type: integer }
        created_at: { type: string, format: date-time }
        finished_at: { type: [string, 'null'], format: date-time }
    Places:
      type: object
      properties:
        results:
          type: array
          items:
            type: object
            properties:
              id: { type: string, example: 'naptan:9100SWANSEA' }
              category: { type: string, enum: [rail_station, metro_station, bus_stop, school] }
              name: { type: string }
              lat: { type: number }
              lng: { type: number }
              distance_m: { type: number }
              source: { type: string, enum: [naptan, gias] }
              details:
                type: object
                description: What the source says of it. A school's has `ofsted`, its latest inspection, with its `framework`, `inspected_on` and `grades`, as Ofsted gives them.
    LocationList:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        location_count: { type: integer }
        seq: { type: integer }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    LocationInput:
      type: object
      properties:
        name: { type: string, maxLength: 200 }
        address:
          type: [object, 'null']
          properties:
            lines: { type: array, maxItems: 4, items: { type: string } }
            town: { type: [string, 'null'] }
            postcode: { type: [string, 'null'] }
        lat: { type: number }
        lng: { type: number }
        phone: { type: [string, 'null'] }
        email: { type: [string, 'null'] }
        url: { type: [string, 'null'] }
        opening_hours: { type: [object, 'null'] }
        tags: { type: array, maxItems: 20, items: { type: string } }
        attributes: { type: object }
        status: { type: string, enum: [active, inactive] }
    Location:
      allOf:
        - { $ref: '#/components/schemas/LocationInput' }
        - type: object
          properties:
            id: { type: string }
            list_id: { type: string }
            geocode_source: { type: string, enum: [customer, zapex] }
            geocode_precision: { type: string, enum: [customer, address] }
            uprn: { type: [string, 'null'] }
            seq: { type: integer }
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }
    WrittenLocation:
      type: object
      properties:
        outcome: { type: string, enum: [created, updated, unchanged] }
        location: { $ref: '#/components/schemas/Location' }
    Origin:
      type: [object, 'null']
      properties:
        lat: { type: number }
        lng: { type: number }
        source: { type: string, enum: [point, postcode, address] }
        postcode: { type: [string, 'null'] }
        uprn: { type: [string, 'null'] }
