{
  "openapi": "3.1.0",
  "info": {
    "title": "Affordable Geo APIs — Geocoding HTTP API",
    "version": "0.2.0",
    "description": "Public geocoding API contract. All operations are GET with query parameters and return a GeoJSON FeatureCollection. Unknown or repeated parameters return 400. Parameter values are trimmed and limited to 256 bytes. Dataset coverage determines geographic availability."
  },
  "servers": [
    {
      "url": "http://localhost:8000",
      "description": "Local development server. Replace with your assigned HTTPS base URL in production."
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "Geocoding",
      "description": "Structured geocoding, free-text search, reverse geocoding, autocomplete, and place lookup."
    }
  ],
  "paths": {
    "/v1/geocode": {
      "get": {
        "operationId": "geocode",
        "tags": [
          "Geocoding"
        ],
        "summary": "Geocode a structured address",
        "description": "Address matches for a house number and street plus at least one of city, state, or zip. Confidence compares the request with each match.",
        "parameters": [
          {
            "name": "street",
            "in": "query",
            "required": true,
            "description": "House number and street.",
            "schema": {
              "type": "string",
              "maxLength": 256,
              "x-maxBytes": 256
            },
            "example": "11 Wall Street"
          },
          {
            "name": "city",
            "in": "query",
            "required": false,
            "description": "City. At least one of city, state, or zip is required.",
            "schema": {
              "type": "string",
              "maxLength": 256,
              "x-maxBytes": 256
            },
            "example": "New York"
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "description": "State or province code or name.",
            "schema": {
              "type": "string",
              "maxLength": 256,
              "x-maxBytes": 256
            },
            "example": "NY"
          },
          {
            "name": "zip",
            "in": "query",
            "required": false,
            "description": "ZIP or postal code; ZIP+4 accepted.",
            "schema": {
              "type": "string",
              "maxLength": 256,
              "x-maxBytes": 256
            },
            "example": "10005"
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "2- or 3-letter country code to restrict results, e.g. US.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z]{2,3}$"
            },
            "example": "US"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum results, 1-40. Default 10.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 40,
              "default": 10
            }
          },
          {
            "name": "exclude_sources",
            "in": "query",
            "required": false,
            "description": "Comma-separated data sources to omit (up to 32; aliases osm, oa, wof accepted).",
            "schema": {
              "type": "string"
            },
            "example": "openstreetmap"
          },
          {
            "name": "min_confidence",
            "in": "query",
            "required": false,
            "description": "Drop results with confidence below this value, 0-1. Default 0. A request left with no results is metered as no_match.",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1,
              "default": 0
            },
            "example": 0.9
          }
        ],
        "responses": {
          "200": {
            "description": "Results (possibly empty).",
            "headers": {
              "X-Geocoding-Outcome": {
                "description": "matched when at least one result is returned, otherwise no_match. Drives usage metering.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "matched",
                    "no_match"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeatureCollection"
                }
              }
            }
          },
          "400": {
            "description": "Missing, unknown, repeated, or invalid query parameter. The message names the problem and, for old parameter names, the replacement.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (may use a `message` field).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Browser token outside its origin or operation scope, or no active subscription or trial.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Account rate limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected geocoder error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The geocoding service returned an invalid response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Geocoding or billing authorization temporarily unavailable; retry with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/search": {
      "get": {
        "operationId": "search",
        "tags": [
          "Geocoding"
        ],
        "summary": "Free-text search",
        "description": "Addresses, places, cities, and ZIP codes matching one line of text.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "What to find.",
            "schema": {
              "type": "string",
              "maxLength": 256,
              "x-maxBytes": 256
            },
            "example": "Starbucks, Los Angeles"
          },
          {
            "name": "near",
            "in": "query",
            "required": false,
            "description": "`lat,lon` to prefer results near a point.",
            "schema": {
              "type": "string",
              "pattern": "^\\s*-?\\d+(\\.\\d+)?\\s*,\\s*-?\\d+(\\.\\d+)?\\s*$"
            },
            "example": "34.05,-118.24"
          },
          {
            "name": "bbox",
            "in": "query",
            "required": false,
            "description": "`minLon,minLat,maxLon,maxLat`; only results inside the box are returned.",
            "schema": {
              "type": "string"
            },
            "example": "-118.7,33.7,-118.1,34.3"
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "2- or 3-letter country code to restrict results, e.g. US.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z]{2,3}$"
            },
            "example": "US"
          },
          {
            "name": "types",
            "in": "query",
            "required": false,
            "description": "Comma-separated result types to include: address, venue, street, zip, neighborhood, borough, city, county, state, country.",
            "schema": {
              "type": "string"
            },
            "example": "address,venue"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum results, 1-40. Default 10.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 40,
              "default": 10
            }
          },
          {
            "name": "exclude_sources",
            "in": "query",
            "required": false,
            "description": "Comma-separated data sources to omit (up to 32; aliases osm, oa, wof accepted).",
            "schema": {
              "type": "string"
            },
            "example": "openstreetmap"
          }
        ],
        "responses": {
          "200": {
            "description": "Results (possibly empty).",
            "headers": {
              "X-Geocoding-Outcome": {
                "description": "matched when at least one result is returned, otherwise no_match. Drives usage metering.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "matched",
                    "no_match"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeatureCollection"
                }
              }
            }
          },
          "400": {
            "description": "Missing, unknown, repeated, or invalid query parameter. The message names the problem and, for old parameter names, the replacement.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (may use a `message` field).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Browser token outside its origin or operation scope, or no active subscription or trial.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Account rate limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected geocoder error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The geocoding service returned an invalid response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Geocoding or billing authorization temporarily unavailable; retry with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/reverse": {
      "get": {
        "operationId": "reverse",
        "tags": [
          "Geocoding"
        ],
        "summary": "Reverse geocode",
        "description": "Addresses, streets, and places near a point, ranked by proximity; containing areas fill remaining results.",
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "description": "Latitude.",
            "schema": {
              "type": "number",
              "minimum": -90,
              "maximum": 90
            },
            "example": 40.7074
          },
          {
            "name": "lon",
            "in": "query",
            "required": true,
            "description": "Longitude.",
            "schema": {
              "type": "number",
              "minimum": -180,
              "maximum": 180
            },
            "example": -74.0113
          },
          {
            "name": "radius",
            "in": "query",
            "required": false,
            "description": "Search radius in meters. Default 1000.",
            "schema": {
              "type": "number",
              "exclusiveMinimum": 0,
              "maximum": 100000
            }
          },
          {
            "name": "types",
            "in": "query",
            "required": false,
            "description": "Comma-separated result types to include: address, venue, street, zip, neighborhood, borough, city, county, state, country.",
            "schema": {
              "type": "string"
            },
            "example": "address,venue"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum results, 1-40. Default 10.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 40,
              "default": 10
            }
          },
          {
            "name": "exclude_sources",
            "in": "query",
            "required": false,
            "description": "Comma-separated data sources to omit (up to 32; aliases osm, oa, wof accepted).",
            "schema": {
              "type": "string"
            },
            "example": "openstreetmap"
          }
        ],
        "responses": {
          "200": {
            "description": "Results (possibly empty).",
            "headers": {
              "X-Geocoding-Outcome": {
                "description": "matched when at least one result is returned, otherwise no_match. Drives usage metering.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "matched",
                    "no_match"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeatureCollection"
                }
              }
            }
          },
          "400": {
            "description": "Missing, unknown, repeated, or invalid query parameter. The message names the problem and, for old parameter names, the replacement.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (may use a `message` field).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Browser token outside its origin or operation scope, or no active subscription or trial.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Account rate limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected geocoder error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The geocoding service returned an invalid response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Geocoding or billing authorization temporarily unavailable; retry with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/autocomplete": {
      "get": {
        "operationId": "autocomplete",
        "tags": [
          "Geocoding"
        ],
        "summary": "Autocomplete",
        "description": "Suggestions for partially typed text; the last word may be partial. Returns full results.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "Text typed so far.",
            "schema": {
              "type": "string",
              "maxLength": 256,
              "x-maxBytes": 256
            },
            "example": "11 Wall"
          },
          {
            "name": "near",
            "in": "query",
            "required": false,
            "description": "`lat,lon` to prefer results near a point.",
            "schema": {
              "type": "string",
              "pattern": "^\\s*-?\\d+(\\.\\d+)?\\s*,\\s*-?\\d+(\\.\\d+)?\\s*$"
            },
            "example": "34.05,-118.24"
          },
          {
            "name": "bbox",
            "in": "query",
            "required": false,
            "description": "`minLon,minLat,maxLon,maxLat`; only results inside the box are returned.",
            "schema": {
              "type": "string"
            },
            "example": "-118.7,33.7,-118.1,34.3"
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "2- or 3-letter country code to restrict results, e.g. US.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z]{2,3}$"
            },
            "example": "US"
          },
          {
            "name": "types",
            "in": "query",
            "required": false,
            "description": "Comma-separated result types to include: address, venue, street, zip, neighborhood, borough, city, county, state, country.",
            "schema": {
              "type": "string"
            },
            "example": "address,venue"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum results, 1-40. Default 10.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 40,
              "default": 10
            }
          },
          {
            "name": "exclude_sources",
            "in": "query",
            "required": false,
            "description": "Comma-separated data sources to omit (up to 32; aliases osm, oa, wof accepted).",
            "schema": {
              "type": "string"
            },
            "example": "openstreetmap"
          }
        ],
        "responses": {
          "200": {
            "description": "Results (possibly empty).",
            "headers": {
              "X-Geocoding-Outcome": {
                "description": "matched when at least one result is returned, otherwise no_match. Drives usage metering.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "matched",
                    "no_match"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeatureCollection"
                }
              }
            }
          },
          "400": {
            "description": "Missing, unknown, repeated, or invalid query parameter. The message names the problem and, for old parameter names, the replacement.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (may use a `message` field).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Browser token outside its origin or operation scope, or no active subscription or trial.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Account rate limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected geocoder error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The geocoding service returned an invalid response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Geocoding or billing authorization temporarily unavailable; retry with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/place": {
      "get": {
        "operationId": "place",
        "tags": [
          "Geocoding"
        ],
        "summary": "Look up results by id",
        "description": "Fetch results by id. Unknown ids are omitted.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "One or more result ids, comma-separated (max 20).",
            "schema": {
              "type": "string"
            },
            "example": "openaddresses:address:us/ny:1"
          }
        ],
        "responses": {
          "200": {
            "description": "Results (possibly empty).",
            "headers": {
              "X-Geocoding-Outcome": {
                "description": "matched when at least one result is returned, otherwise no_match. Drives usage metering.",
                "schema": {
                  "type": "string",
                  "enum": [
                    "matched",
                    "no_match"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeatureCollection"
                }
              }
            }
          },
          "400": {
            "description": "Missing, unknown, repeated, or invalid query parameter. The message names the problem and, for old parameter names, the replacement.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key (may use a `message` field).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Browser token outside its origin or operation scope, or no active subscription or trial.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Account rate limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected geocoder error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The geocoding service returned an invalid response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Geocoding or billing authorization temporarily unavailable; retry with backoff.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "apikey",
        "description": "Credential in the apikey header only (never the URL). Server keys support all operations. Public pub_ browser tokens support only GET /v1/autocomplete and GET /v1/place and require an exact allowed Origin; other operations or origins return 403. CORS preflight requests do not require authentication."
      }
    },
    "schemas": {
      "ResultProperties": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "label",
          "type",
          "name",
          "house_number",
          "street",
          "unit",
          "neighborhood",
          "city",
          "county",
          "state",
          "state_name",
          "zip",
          "country",
          "lat",
          "lon",
          "confidence",
          "match_type",
          "distance_m",
          "categories",
          "source"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable result id; look it up again with /v1/place."
          },
          "label": {
            "type": "string",
            "description": "Full display address."
          },
          "type": {
            "type": "string",
            "enum": [
              "address",
              "venue",
              "street",
              "zip",
              "neighborhood",
              "borough",
              "city",
              "county",
              "state",
              "country"
            ],
            "description": "What was matched."
          },
          "name": {
            "type": "string",
            "description": "Short name of the result."
          },
          "house_number": {
            "type": [
              "string",
              "null"
            ],
            "description": "House number."
          },
          "street": {
            "type": [
              "string",
              "null"
            ],
            "description": "Street name without the house number."
          },
          "unit": {
            "type": [
              "string",
              "null"
            ],
            "description": "Unit, suite, or apartment."
          },
          "neighborhood": {
            "type": [
              "string",
              "null"
            ],
            "description": "Neighborhood."
          },
          "city": {
            "type": [
              "string",
              "null"
            ],
            "description": "City, town, or township."
          },
          "county": {
            "type": [
              "string",
              "null"
            ],
            "description": "County."
          },
          "state": {
            "type": [
              "string",
              "null"
            ],
            "description": "State or province code, or its name when no code exists."
          },
          "state_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Full state or province name."
          },
          "zip": {
            "type": [
              "string",
              "null"
            ],
            "description": "ZIP or postal code."
          },
          "country": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 3166-1 alpha-2 country code."
          },
          "lat": {
            "type": "number",
            "minimum": -90,
            "maximum": 90,
            "description": "Latitude."
          },
          "lon": {
            "type": "number",
            "minimum": -180,
            "maximum": 180,
            "description": "Longitude."
          },
          "confidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Geocode: address-evidence score (max 0.99). Other endpoints: ranking/distance score."
          },
          "match_type": {
            "type": "string",
            "enum": [
              "exact",
              "interpolated",
              "fallback"
            ],
            "description": "How the result matched."
          },
          "distance_m": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Meters from the near point (search/autocomplete) or query point (reverse)."
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Venue categories; empty for other types."
          },
          "source": {
            "type": "string",
            "description": "Data source, e.g. openaddresses."
          }
        }
      },
      "Feature": {
        "type": "object",
        "required": [
          "type",
          "geometry",
          "properties"
        ],
        "properties": {
          "type": {
            "const": "Feature"
          },
          "geometry": {
            "type": "object",
            "required": [
              "type",
              "coordinates"
            ],
            "properties": {
              "type": {
                "const": "Point"
              },
              "coordinates": {
                "type": "array",
                "items": {
                  "type": "number"
                },
                "minItems": 2,
                "maxItems": 2,
                "description": "[lon, lat]"
              }
            }
          },
          "properties": {
            "$ref": "#/components/schemas/ResultProperties"
          },
          "bbox": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "minItems": 4,
            "maxItems": 4,
            "description": "[minLon, minLat, maxLon, maxLat]"
          }
        }
      },
      "FeatureCollection": {
        "type": "object",
        "required": [
          "type",
          "features"
        ],
        "properties": {
          "type": {
            "const": "FeatureCollection"
          },
          "features": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Feature"
            }
          },
          "bbox": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "minItems": 4,
            "maxItems": 4,
            "description": "Box around all results; absent when there are none."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      }
    }
  }
}
