---
title: "Structured geocode — component fields into coordinates"
method: POST
path: "/v1/search/structured"
tags: ["Geocoding"]
---

# Structured geocode — component fields into coordinates

`POST /v1/search/structured`

## Request

- Base URL: `https://geo.thinair.co`
- URL: `https://geo.thinair.co/v1/search/structured`
- Auth: HTTP bearer

Forward geocode from discrete address components (address / locality / region / postalcode / country) instead of one free-text string. Skips the free-text parser entirely, so pre-fielded data (CRM records, checkout forms, fleet stop tables) can't be mis-tokenized. Response shape is identical to GET /v1/geocode. Costs 1 quota unit; responses are edge-cached (X-Cache: HIT/MISS). For named cross-streets use POST /v1/intersection.

## Request body

- StructuredGeocodeRequest: Component-wise geocode request. At least one component field is required. There is no cross-street field — use POST /v1/intersection for named cross-streets.
  - `address` string: Street address — house number + street name.
  - `locality` string: City / town name.
  - `region` string: State / province — full name or abbreviation (e.g. 'DC').
  - `postalcode` string: Postal / ZIP code.
  - `country` string: ISO 3166-1 alpha-2/alpha-3 country code (full names also resolve).
  - `size` integer: Maximum candidates to return (1–40). Defaults to 5.
  - `boundary_country` string: Scope results to this ISO country code — wins over `country` for boundary filtering.
  - `focus_lat` number: Latitude of the proximity bias point.
  - `focus_lon` number: Longitude of the proximity bias point.

## Response `200`

Geocoder result envelope (same contract as GET /v1/geocode).

- GeocodeResponse
  - `features` GeocodedFeature[], required
    - `id` string: Stable public ThinAir id (`ta_place_<key>` / `ta_intersection_<key>`). Deterministic across re-serves; resolvable via `GET /v1/place`. Absent on synthetic curated features.
    - `name` string
    - `label` string
    - `confidence` number
    - `layer` string
    - `intersection_type` 'corner' | 'interchange'
    - `intersection_precision` 'geometric_crossing' | 'tangent_intersection' | 'centroid_cluster' | 'approximate'
    - `street_kind` 'street' | 'route'
    - `cross_street_kind` 'street' | 'route'
    - `street_route` string
    - `cross_street_route` string
    - `place_type` string, required
    - `confidence_scope` 'query_to_candidate', required: What `confidence` is scoped to. `query_to_candidate` means it scores how well this candidate matches the submitted query — it is NOT a probability that the place is real, that the address is correct, or that mail would arrive. Fixed value today; treat it as a closed set that may gain members.
    - `address_evidence` object, required: What THIS CANDIDATE carries, component by component — `matched` (a real value is present), `partial` (only a coarser or derived stand-in, e.g. an interpolated house number or a locality falling back to its administrative parent), `absent` (nothing). This describes the result, NOT a comparison against your query: the engine returns no per-component match data, and re-parsing your text here would mean two parsers disagreeing about the same string. Derived from the feature, never from `confidence`.
      - `house_number` 'matched' | 'partial' | 'absent', required
      - `street` 'matched' | 'partial' | 'absent', required
      - `locality` 'matched' | 'partial' | 'absent', required
      - `region` 'matched' | 'partial' | 'absent', required
      - `postal_code` 'matched' | 'partial' | 'absent', required: Whether the candidate carries a postal code. Presence only — this states what the result has, and makes no claim about its correctness.
    - `postal_code` string: An eligible US ZIP5 read from the selected result's own postal-code field. While origin enforcement is active this field is emitted ONLY when the engine stamped the value as coming from the address record itself (`source_record` provenance); a value inherited from a parent area, or a result cached before that evidence existed, is omitted rather than emitted without provenance — so an absent ZIP under enforcement means "not proven for this record", not "no such ZIP". Provenance describes only WHERE the value came from. It is not a postal operator check of any kind — no certification, no address-quality rating, and no statement about whether mail would arrive — so a syntactically correct, correctly-sourced value can still be wrong for the address. Also omitted when unavailable or unsuitable for five-digit form. US address results only. Distinct from `address_evidence.postal_code`, which is a presence STATUS rather than a value. On reverse geocoding this describes the RETURNED ADDRESS — it is not a postal assignment to the coordinate you supplied.
    - `postal_input_match` 'match' | 'conflict' | 'unavailable': STRUCTURED GEOCODING ONLY (`geocode_structured` / `POST /v1/search/structured`); absent from free-text geocoding. Whether the postcode YOU SUBMITTED agrees with this candidate's own postcode — `match` (same five-digit value), `conflict` (they differ), `unavailable` (no comparison was possible). This compares two values already in hand: no lookup, no external reference, and your query text is never parsed to find one. `conflict` does NOT say which side is wrong — your input may name a neighbouring area, the candidate may be a near-miss, or the record's own value may be stale; treat it as a prompt to ask a human, never as a correction to apply automatically. The candidate side counts only when its postcode came from the address record itself; one inherited from a parent area yields `unavailable` rather than a misleading verdict. `unavailable` also covers: nothing submitted, a submitted value not readable as a US five-digit code, and results cached before this field existed.
    - `address_line1` string: House number and street for an eligible US address result, composed from the result's own components — never parsed from the display label. Absent when the result is not a US street address.
    - `address_line2` string: Unit or suite, only when the source record carries one. A unit supplied in your request is never echoed back here.
    - `formatted_address` string: Single-line US display form, e.g. `1600 Pennsylvania Avenue NW, Washington, DC 20500, USA`. The postal segment is present only when an eligible ZIP is; its absence never suppresses the street address.
    - `address_components` object: The same US components the formatted string is built from. Every field is source-established or absent; nothing is inferred or back-filled.
      - `house_number` string
      - `street` string
      - `unit` string
      - `locality` string
      - `region_code` string
      - `region_name` string
      - `postal_code` string
      - `country_code` string
    - `method` string, required
    - `lat` number, required
    - `lon` number, required
  - `meta` GeocodeMeta, required
    - `upstream_ms` number, required
    - `feature_count` number, required
    - `cacheHit` boolean
    - `bbox` unknown[]
      - unknown
    - `query` string, required
    - `mode` 'geocode' | 'reverse', required
  - `geojson` GeocodeFeatureCollection, required
    - `type` 'FeatureCollection', required
    - `features` object[], required
      - `type` 'Feature', required
      - `geometry` object, required
        - `type` 'Point', required
        - `coordinates` unknown[], required
          - unknown
      - `properties` object, required
  - `warnings` string[]: Geocoder warnings (`geocoding.warnings`) — present only when the suppress middleware emitted signals on this response. Treat as opaque strings.
  - `precision` PrecisionBlock: Learning-overlay precision adjudication. Present only when the overlay evaluated this query (intersection-class). Omitted otherwise — legacy consumers see no shape change.
    - `class` string, required
    - `risk_flags` string[], required
    - `required_context` string[], required
    - `learning_overlay` object, required
      - `applied` boolean, required
      - `rule_ids` string[]
      - `actions` string[]
  - `upstream_error` string, nullable

## Other responses

- `400`: Invalid request body (including no component fields at all).
- `429`: Quota exceeded.
- `502`: Upstream geocoder unavailable.

---

[API](https://skmtc.dev/thinair/apis/thinair-geo-api.md) · [All operations](https://skmtc.dev/thinair/apis/thinair-geo-api/llms.txt) · [OpenAPI document](https://skmtc.dev/thinair/apis/thinair-geo-api/revisions/8fac2d5e4b34?raw)
