> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mireye.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# POST /v1/proximity

> Drive-time distance, nearest-candidate, proximity screening, and labor-shed compute across multiple US + Canada coordinates.

<Note>
  **Previously referred to as `/v1/compute` in early design docs.** The endpoint
  was renamed to `/v1/proximity` before its first public release, so there is no
  `/v1/compute` to migrate from — nothing ever served under that path. If you
  find `compute` in our module names or internal symbols, that is the
  implementation package, not an alternate endpoint.
</Note>

## When to use this

`/v1/fetch` and `/v1/ask` answer questions about **one** coordinate.
`/v1/proximity` answers questions about the **relationship between** coordinates
— how far, how long, which is closest, which sites clear a drive-time bar,
how many workers live within a commute. It's a matrix operation, not a
per-point lookup.

One request body, discriminated on `op`:

| `op`         | Answers                                                                                                    |
| ------------ | ---------------------------------------------------------------------------------------------------------- |
| `distance`   | Driving (or straight-line) distance and duration for every origin × destination pair.                      |
| `nearest`    | The closest N candidates from a curated set (airports, substations, …), ranked by drive time.              |
| `screen`     | Which origins are within a drive-time band of ANY of up to 10 anchors — and which missed, and by how much. |
| `labor_shed` | Civilian labor force + population reachable from one origin within N drive-time minutes.                   |

Every response, whatever the op, carries `paid_driving_calcs` (exactly what
pricing charged for — see [Pricing](#pricing)) and `notes` (the honesty labels
below). Errors share one taxonomy — see [Errors](#errors).

## Coverage and honesty labels

* **US + Canada.** Every response's `notes` includes `"coverage: US +
  Canada"`, regardless of op or mode.
* **Durations reflect typical traffic, not real-time conditions.** Every
  response that actually drives — every `screen`/`labor_shed` call, and any
  `distance`/`nearest` call with `mode: "driving"` — adds
  `"durations reflect typical traffic, not real-time"` to `notes`. Don't use
  this for "how long right now"; use it for "how long, typically."
* **`mode: "straightline"`** (available on `distance` and `nearest` only)
  never calls a routing provider — it's a pure geodesic (great-circle)
  distance, computed locally, free. `duration_seconds`/`duration_minutes` are
  always `null` in this mode (no traffic note either, since nothing was
  timed), because there's no route to time.

## Locators: a coordinate or a street address — never a place name

Every origin, destination, and anchor is either a `"lat,lng"` string or a US
street address. **It is never a place name.** "JFK Airport" and "the Tesla
Gigafactory" are not addresses, and passing one resolves as an ordinary
(unresolvable) address string — it does not fall back to a search.

To reach named infrastructure, use `nearest`'s curated `set` parameter
(`@airports`, `@substations`, `@power_plants`, `@rail`, `@ports`,
`@urban_areas` — see [`nearest`](#nearest-top-n-from-a-curated-set)) or supply
that place's own coordinate directly.

This is deliberate, not a missing feature: Geocodio's distance API only ever
receives coordinates that `/v1/proximity` has already resolved server-side —
never a caller's raw locator string. That's what keeps one bad address from
failing an entire N×M matrix (a single bad geocode used to be able to 422 the
whole request), what lets the same accuracy gate `/v1/geocode` enforces apply
uniformly here, and why a POI name can't quietly resolve to whatever a text
search happens to guess.

### Be as precise as you can — vague locators fail *quietly* upstream

The failure mode to design against is not "no match found." It's a
**confident match on the wrong place.** Two real examples:

| You send               | Geocoder matches    | What it is                             |
| ---------------------- | ------------------- | -------------------------------------- |
| `"1412 market street"` | `Market, WV 26411`  | a town in West Virginia named Market   |
| `"SFO airport"`        | `Airport, NC 28219` | a town in North Carolina named Airport |

Neither returns an error upstream. `"SFO airport"` comes back at **confidence
1.0**. There is no second candidate to compare against — asking for five
returns exactly one. The only thing standing between that and a 2,400-mile
drive time reported as fact is our accuracy gate, which refuses both because
they resolved to `place` tier rather than to a street or a rooftop.

So the gate saves you, but only from the coarse cases. Give it as much as you
have:

* **Always include city + state, or a ZIP.** A bare street line is the single
  most common cause of a wrong match — Market Streets and Main Streets exist
  in hundreds of towns, and the geocoder will happily pick one.
* **Never send a landmark, business, or airport name.** Use its coordinate, or
  `nearest`'s curated `set`.
* **Prefer coordinates for anything you resolve repeatedly.** They skip the
  gate, cost no geocoding credit, and can't drift.
* **A gate pass is not a correctness guarantee.** US territories are a known
  hole: a Puerto Rico address can match a *different* PR address at rooftop
  tier and 0.99 confidence. Check `formatted_address` in the response against
  what you sent whenever the stakes are high.

<Warning>
  **If you fill in a missing piece yourself, say so.** An LLM agent can often
  repair `"1412 market street"` to `"1412 Market St, San Francisco, CA"` by
  inferring the city from surrounding context — a plain program can't. That
  repair is useful, and it is invisible unless you disclose it. Our response
  will carry a real rooftop-accuracy address and look authoritative even though
  the city came from you, not from your user. Report the assumption alongside
  the answer ("the address had no city; I assumed San Francisco"), and ask
  rather than guess when you aren't confident. The `422` carries this rule as
  `caller_guidance` so it's readable at the point of failure.
</Warning>

### The accuracy gate

An address resolves through the same floor `/v1/geocode` enforces — below
parcel/street-tier precision or below 0.8 similarity, it's refused rather than
guessed. Every locator's outcome is echoed back as a `ResolvedPoint`:

| `error`                     | Meaning                                                                                                                                                                        | `lat`/`lng` |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- |
| *(absent)*                  | Resolved. Coordinate strings resolve instantly; addresses carry `formatted_address`/`accuracy_type`/`accuracy` from the match.                                                 | populated   |
| `unresolvable_input`        | The provider found no match at all.                                                                                                                                            | `null`      |
| `low_confidence_resolution` | The provider matched something, but it failed the accuracy gate. `formatted_address`/`accuracy_type` describe what was *rejected* — we don't echo a coordinate we don't trust. | `null`      |
| `geocoding_failed`          | A transient, auth, quota, or unsupported-address-form failure on this one item.                                                                                                | `null`      |

Here's the gate catching a live case: `"1450 Ridgecrest Dr, CA"` names a
street and a state but no city, and the upstream doesn't report no match — it
silently returns the centroid of the *city of Ridgecrest*, which is a real
place hundreds of miles from most streets of that name:

```json theme={null}
{
  "query": "1450 Ridgecrest Dr, CA",
  "lat": null,
  "lng": null,
  "formatted_address": "Ridgecrest, CA 93555",
  "accuracy_type": "place",
  "accuracy": null,
  "error": "low_confidence_resolution"
}
```

**A coordinate string skips the gate entirely** — there's no confidence
signal on a bare `"lat,lng"` to gate on.

### Per-item failures never fail the whole request — except for a required role

A `distance`/`screen` request's bulk **destinations** degrade per-item: a
failed one is omitted from `legs`, everything else still computes, and each
one's outcome is echoed in `resolved_destinations`. `nearest`'s candidates
degrade too, but with less detail — they are excluded from the ranking and
counted in `notes`, with no per-candidate echo. `paid_driving_calcs` is
still priced from the full request shape, not from what happened to resolve —
see [Pricing](#pricing) for why that matters.

A **required role** — every op's origin(s), or `screen`'s anchors — fails the
whole request with `422 unresolvable_input` if every item in that role fails.

<Warning>
  **A `200` can be missing rows you asked for.** Send 5 destinations, have 2
  fail, and you get back 3 legs and no error. Every drop is announced in
  `notes`, so read it before summarising a result — "the 3 nearest" is a false
  statement if 2 of the 5 never resolved. What you get per op:

  * **`distance` / `screen`** — a `notes` entry saying how many failed and in
    which role, plus the `resolved_origins` / `resolved_destinations` /
    `resolved_anchors` arrays naming which ones and why.
  * **`nearest`** — a `notes` entry counting candidates excluded for having no
    usable road route. There is no per-candidate echo on this op, so the count
    is the only signal: asking for `n: 3` can legitimately return two.
  * **Any driving leg** — `flag: "unreachable_or_snapped"` with a `null`
    duration means no road route was found. Don't present its distance as
    drivable.
  * **`labor_shed`** — `tracts_unreachable` counts tracts excluded the same way.
</Warning>

## `distance` — N origins × M destinations

```jsonc theme={null}
{
  "op": "distance",
  "origins": ["40.630973,-73.97228"],       // 1–500 locators
  "destinations": ["40.6413,-73.7781"],     // 1–500 locators
  "mode": "driving",                        // "driving" | "straightline", default "driving"
  "units": "miles"                          // reserved; both distance_miles and distance_km are always returned
}
```

```json theme={null}
{
  "op": "distance",
  "legs": [
    {
      "origin_index": 0,
      "destination_index": 0,
      "distance_miles": 13.8,
      "distance_km": 22.2,
      "duration_seconds": 1650,
      "duration_minutes": 27.5,
      "flag": null
    }
  ],
  "resolved_origins": [
    {"query": "40.630973,-73.97228", "lat": 40.630973, "lng": -73.97228, "formatted_address": null, "accuracy_type": null, "accuracy": null, "error": null}
  ],
  "resolved_destinations": [
    {"query": "40.6413,-73.7781", "lat": 40.6413, "lng": -73.7781, "formatted_address": null, "accuracy_type": null, "accuracy": null, "error": null}
  ],
  "paid_driving_calcs": 1,
  "notes": ["coverage: US + Canada", "durations reflect typical traffic, not real-time"]
}
```

That's a real measured pair (Brooklyn → JFK): 13.8 mi / 22.2 km driven in
about 27.5 minutes of typical traffic.

Mixing an address with a coordinate, and a locator that fails to resolve,
in one request:

```jsonc theme={null}
{
  "op": "distance",
  "origins": ["350 5th Ave, New York, NY 10118"],
  "destinations": ["JFK Airport", "40.6413,-73.7781"]
}
```

`"JFK Airport"` is a place name, not an address — see
[Locators](#locators-a-coordinate-or-a-street-address--never-a-place-name).
It resolves as an ordinary unresolvable address string:

```json theme={null}
{
  "op": "distance",
  "legs": [
    { "origin_index": 0, "destination_index": 1, "distance_miles": 13.9, "distance_km": 22.4, "duration_seconds": 1680, "duration_minutes": 28.0, "flag": null }
  ],
  "resolved_origins": [
    {"query": "350 5th Ave, New York, NY 10118", "lat": 40.748377, "lng": -73.984854, "formatted_address": "350 5th Ave, New York, NY 10118", "accuracy_type": "rooftop", "accuracy": 1.0, "error": null}
  ],
  "resolved_destinations": [
    {"query": "JFK Airport", "lat": null, "lng": null, "formatted_address": null, "accuracy_type": null, "accuracy": null, "error": "unresolvable_input"},
    {"query": "40.6413,-73.7781", "lat": 40.6413, "lng": -73.7781, "formatted_address": null, "accuracy_type": null, "accuracy": null, "error": null}
  ],
  "paid_driving_calcs": 2,
  "notes": ["coverage: US + Canada", "durations reflect typical traffic, not real-time"]
}
```

Note `legs` has one entry (the destination that resolved), but
`paid_driving_calcs` is **2** — one origin × two destinations, the request's
shape, computed before either locator was resolved. You're charged for what
you asked for, not for what happened to work.

### The snap guard

A driven leg **shorter** than 95% of the straight-line distance between the
same two points is not a real road route — it's the provider silently
snapping an unreachable point (an island with no bridge, a spot in open
water) to the nearest road. This is a real, verified case: Los Angeles to
Catalina Island — no road or bridge crosses that water, and the routing
provider still returns a confident 200 with a "drive." `/v1/proximity` computes
the straight-line distance locally for every leg and flags this as
impossible rather than passing that through:

```json theme={null}
{ "origin_index": 0, "destination_index": 0, "distance_miles": 26.3, "distance_km": 42.3, "duration_seconds": null, "duration_minutes": null, "flag": "unreachable_or_snapped" }
```

(26.3 driven miles against a straight-line distance of about 49 miles — far
short of what any real road route to the island would require.)

Distances stay visible; `duration_seconds`/`duration_minutes` are nulled,
because a duration for a route that doesn't exist isn't a number worth
trusting. (Real routes legitimately run *longer* than straight-line — a
15–20% detour factor is normal — so the guard only fires on the geometrically
impossible direction.)

## `nearest` — top-N from a curated set

```jsonc theme={null}
{
  "op": "nearest",
  "origin": "40.630973,-73.97228",
  "set": "@airports",
  "n": 3,                 // 1–25, default 3
  "filters": null,        // set-specific override, see below
  "mode": "driving"        // "driving" | "straightline"
}
```

```jsonc theme={null}
{
  "op": "nearest",
  "origin": {"query": "40.630973,-73.97228", "lat": 40.630973, "lng": -73.97228, "formatted_address": null, "accuracy_type": null, "accuracy": null, "error": null},
  "candidates": [
    {
      "name": "JOHN F KENNEDY INTL",
      "lat": 40.6413, "lng": -73.7781,
      "attributes": {"facility_type": "airport", "use": "PU"},
      "distance_miles": 13.8, "distance_km": 22.2,
      "duration_seconds": 1650, "duration_minutes": 27.5
    }
    // ...up to n candidates, ranked by duration ascending
  ],
  "applied_filters": null,
  "paid_driving_calcs": 15,
  "notes": ["coverage: US + Canada", "durations reflect typical traffic, not real-time"]
}
```

There's no `"origin_name"`-style search — `set` names one of six curated
destination sets, each backed by a public federal dataset already ingested
into the catalog:

| `set`           | Backing data                | Default class filter                                                                                                                                                                      | Override                                                                                                         |
| --------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `@airports`     | FAA NASR (28-day cycle)     | Public-use airports only — `facility_type == "airport"` (heliports excluded) and `use` is `"PU"` or unrated (a row with no `use` value is INCLUDED; only an explicit `"PR"` is excluded). | none in v1                                                                                                       |
| `@substations`  | EIA/HIFLD power substations | `max_voltage_kv >= 115`. A substation with **no published voltage is excluded** — it can't prove it clears the threshold.                                                                 | `{"min_kv": <n>}`, e.g. `{"min_kv": 0}` to see every *rated* substation (unrated ones stay excluded regardless). |
| `@power_plants` | EIA power plants            | none — every in-range plant is a candidate.                                                                                                                                               | —                                                                                                                |
| `@rail`         | BTS NTAD rail network       | none.                                                                                                                                                                                     | —                                                                                                                |
| `@ports`        | BTS maritime ports          | none.                                                                                                                                                                                     | —                                                                                                                |
| `@urban_areas`  | Census TIGER urban areas    | none.                                                                                                                                                                                     | —                                                                                                                |

Two things worth knowing before you rely on this:

* **The search radius is fixed at 160 km (\~100 mi) and isn't configurable in
  v1.** If nothing qualifies within that radius, `candidates` comes back
  empty — not an error.
* **`applied_filters` echoes exactly what you sent, not the default that ran
  when you sent nothing.** If you call `@substations` with no `filters`, the
  115 kV floor above still applies, but `applied_filters` reads `null`. Pass
  `filters` explicitly if you need the response to say what threshold was
  used.

`paid_driving_calcs` for `nearest` is `min(25, n × 5)` — a fixed multiple of
`n`, not the number of candidates actually found or returned, so it's
knowable before the call.

## `screen` — proximity filter against up to 10 anchors

Filters a batch of origins by drive-time proximity to **any** of up to 10
anchors — e.g., "which of these 40 parcels are within 20 minutes of I-95's
Exit 12 (`40.71,-73.99`) AND at least 10 minutes from downtown." Anchors are
`"lat,lng"` coordinates or street addresses, the same as every other locator
in this API — `screen` has no curated `set` parameter (that's `nearest`'s),
so there is no shortcut for "the nearest interstate on-ramp"; supply the
on-ramp's own coordinate.

```jsonc theme={null}
{
  "op": "screen",
  "origins": ["40.70,-73.95", "40.75,-73.80", "40.90,-73.70"],
  "anchors": ["40.6413,-73.7781"],   // 1–10 locators
  "max_minutes": 30,
  "min_minutes": null                // optional lower bound
}
```

```json theme={null}
{
  "op": "screen",
  "survivors": [
    {"origin_index": 0, "best_anchor_index": 0, "best_duration_seconds": 1080, "best_duration_minutes": 18.0}
  ],
  "screened_out": [
    {"origin_index": 1, "best_duration_seconds": 2220, "best_duration_minutes": 37.0},
    {"origin_index": 2, "best_duration_seconds": null, "best_duration_minutes": null}
  ],
  "resolved_origins": [ "…" ],
  "resolved_anchors": [ "…" ],
  "paid_driving_calcs": 3,
  "notes": ["coverage: US + Canada", "durations reflect typical traffic, not real-time"]
}
```

**A non-survivor is never dropped silently.** `screened_out` reports each
origin's own best duration against ANY anchor, even though it missed the
band — that's the near-miss, and it's the whole reason `max_minutes`/
`min_minutes` are enforced locally rather than sent to the routing provider:
an upstream duration filter makes a failing leg vanish with no marker (and
still bills for it), which is unusable for "how close did it come."
`best_duration_seconds: null` (the third origin above) means every anchor
leg was unreachable or snapped — not just slow.

`min_minutes` is a **lower** bound — use it to exclude an origin that's too
close to every anchor (e.g., you want 10–30 minutes from a highway, not right
on top of it). `screen` always drives; there is no `straightline` mode, since
"screen by proximity" without traffic-aware duration isn't the use case.

`paid_driving_calcs` = `len(origins) × len(anchors)` — the full matrix, always
computed, regardless of how many survive.

## `labor_shed` — civilian labor force + population within a drive time

```jsonc theme={null}
{
  "op": "labor_shed",
  "origin": "32.9,-96.9",
  "minutes": 45          // 5–90
}
```

```json theme={null}
{
  "op": "labor_shed",
  "origin": {"query": "32.9,-96.9", "lat": 32.9, "lng": -96.9, "formatted_address": null, "accuracy_type": null, "accuracy": null, "error": null},
  "civilian_labor_force": 612340,
  "population": 1024500,
  "tracts_counted": 812,
  "tracts_matrix_queried": 40,
  "tracts_unreachable": 1,
  "minutes": 45,
  "paid_driving_calcs": 40,
  "notes": ["coverage: US + Canada", "durations reflect typical traffic, not real-time"]
}
```

This sums two Census-tract fields — civilian labor force (ACS 5-year,
B23025) and total population (CenPop2020) — over every tract reachable from
`origin` within `minutes` of driving. Rather than routing every tract in the
country, it classifies each candidate tract by pure geometry first, which is
free:

* **Definitely reachable** — the tract's straight-line distance is under a
  20 mph bound. No real route can be slower than a straight line at that
  speed over that short a distance, so it counts without a routed check.
* **Definitely unreachable** — straight-line distance exceeds a 75 mph
  bound. No road beats a straight line, so it's excluded without a routed
  check.
* **The annulus** — everything between those two bounds is genuinely
  uncertain and gets one real driving-matrix call per tract centroid.
  `tracts_matrix_queried` is exactly this count, and it **is**
  `paid_driving_calcs` for `labor_shed` — you're charged for the tracts that
  actually needed a routed answer, not the ones geometry already settled.

Every annulus leg is also checked with the same geodesic snap guard every
other `/v1/proximity` driving op applies: a leg shorter than 95% of its
straight-line distance is a snap-to-nearest-road artifact (an island tract
snapped to a nearby mainland road, say), not a real route — even when its
reported duration looks in-budget. A flagged tract is excluded from both
sums and from `tracts_counted`, but it's still counted in
`tracts_matrix_queried` (it was still queried) and in `tracts_unreachable`,
so a trimmed shed is visible rather than silently over-counted.

A `labor_shed` query over an entirely near or entirely far origin — a small
`minutes` in a sparse area, say — can cost **zero** paid driving calcs and
still bill the 25-credit floor. The annulus is capped at 3,000 tracts;
past that, the request fails loud with `422 shed_too_large` rather than
running an enormous, slow matrix.

`civilian_labor_force` skips any tract whose ACS estimate is null (a sample
the Census Bureau couldn't produce contributes nothing); `population` counts
every included tract regardless — a tract with unknown labor force still has
known people living in it.

### Just want your own tract's numbers, not a shed?

The same two figures are also ordinary catalog fields — `tract_civilian_labor_force`
and `tract_population` — fetchable for a single point via `/v1/fetch`, at the
standard per-field credit price, with no `/v1/proximity` call needed:

```bash theme={null}
curl -s https://api.mireye.com/v1/fetch \
  -H "Authorization: Bearer $MIREYE_API_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"lat": 32.9, "lng": -96.9, "fields": ["tract_civilian_labor_force", "tract_population"]}' | jq
```

```json theme={null}
{
  "fields": {
    "tract_civilian_labor_force": {
      "value": 1800,
      "unit": "people",
      "source": "CENSUS_TRACT_WORKFORCE",
      "source_url": "https://www.census.gov/programs-surveys/acs/data/summary-file.html",
      "confidence": "medium",
      "dataset_vintage": "ACS 2019-2023 5-year civilian labor force (B23025) + 2020 Census population-weighted tract centroid (CenPop2020) population",
      "notes": "tract_civilian_labor_force for Census tract 48113017103"
    },
    "tract_population": {
      "value": 3120,
      "unit": "people",
      "source": "CENSUS_TRACT_WORKFORCE",
      "confidence": "medium"
    }
  }
}
```

Reach for `/v1/fetch` when you want the home tract's own figures; reach for
`labor_shed` when you want the total across everywhere reachable within a
drive time, which is almost never just the home tract.

## Pricing

Every response echoes `paid_driving_calcs` — priced from the **request
shape** (or, for `labor_shed`, the annulus size after the free geometric
prefilter), never from how many locators happened to resolve, so the price is
always knowable in advance:

```
credits = max(op_floor, 12 × paid_driving_calcs) + 1 × address-form locators
```

Floors exist so a straightline or memo-served request never bills \$0, and so
a 1-tract `labor_shed` doesn't undercharge for the fixed cost of running it:

| `op`         | Floor (credits) |
| ------------ | --------------- |
| `distance`   | 2               |
| `nearest`    | 2               |
| `screen`     | 5               |
| `labor_shed` | 25              |

**Address-form locators cost extra, additively.** Any origin/destination/
anchor that isn't a `"lat,lng"` coordinate triggers its own forward-geocoding
call against the same shared quota `POST /v1/geocode` draws from, so it's
priced at that same rate: **+1 credit per address-form locator**, on top of
the driving-calc price above — a coordinate locator never adds anything.
Capped at **25 address-form locators per request** (across
origins+destinations, or origins+anchors on `screen`) — a request over that
cap is rejected `422` before any billing, same as the total-calc cap below.

Worked examples:

| Request                                              | `paid_driving_calcs` | Credits charged |
| ---------------------------------------------------- | -------------------- | --------------- |
| `distance`, 1×1, `mode: "straightline"`              | 0                    | **2** (floor)   |
| `distance`, 1×1, `mode: "driving"`                   | 1                    | **12**          |
| `distance`, 10 origins × 20 destinations, driving    | 200                  | **2,400**       |
| `distance`, 1×1, driving, 1 address-form destination | 1                    | **13** (12 + 1) |
| `nearest`, default `n: 3`, `mode: "straightline"`    | 0                    | **2** (floor)   |
| `nearest`, default `n: 3`                            | 15 (`min(25, n×5)`)  | **180**         |
| `screen`, 20 origins × 3 anchors                     | 60                   | **720**         |
| `labor_shed`, entirely inside the 20 mph inner band  | 0                    | **25** (floor)  |
| `labor_shed`, 40 tracts in the annulus               | 40                   | **480**         |

Credits are debited from your plan's shared pool — the same one every other
endpoint draws from. `/v1/meta/plans` publishes the constants above
(`proximity_per_driving_calc`, `proximity_distance_min`, `proximity_nearest_min`,
`proximity_screen_min`, `proximity_labor_shed_min`, `proximity_per_address_locator`)
alongside your plan's credit-to-dollar rate.

**Which failures are billed.** The price is debited in two parts, and there
are no refunds, so what a failure costs depends on which parts it reached:

* the **geocoding** part (`+1` per address-form locator) is debited *before*
  your locators are resolved, because resolving them is what makes those
  calls;
* the **driving** part (`max(op_floor, 12 × paid_driving_calcs)`) is debited
  as late as it safely can be: after every required locator has resolved,
  after the curated set has been looked up, and after the per-request budget
  share has been checked — but still before the driving matrix is called.

The geocoding part is debited **before the op runs**, so any failure that
happens after your locators were resolved has already paid for those lookups.
"Not billed" below therefore always means *the driving part is not billed* —
a request made entirely of coordinates costs literally nothing in those rows,
while one carrying address-form locators still owes 1 credit per address:

| Failure                                                          | Driving part                                                 | Geocoding part                                                   |
| ---------------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------- |
| `422 invalid_request` (schema, including the per-request caps)   | **No**                                                       | **No** — rejected before the handler runs at all.                |
| `429 proximity_busy`                                             | **No**                                                       | **No** — the overload gate precedes all metering.                |
| `422 unresolvable_input` (any op)                                | **No** — the matrix never ran.                               | **Yes** — those lookups are exactly what discovered the failure. |
| `422 unknown_set` on `nearest`                                   | **No** — a bad `set` ref does no upstream work.              | **Yes**, if the origin was an address.                           |
| `422 shed_too_large` on `labor_shed`                             | **No** — the annulus is sized by a free geometric prefilter. | **Yes**, if the origin was an address.                           |
| `422 proximity_request_exceeds_budget_share`                     | **No** — refused before it reserves anything.                | **Yes**, if any locator was an address.                          |
| `503 proximity_data_unavailable`                                 | **No** — a missing backing asset is our failure.             | **Yes**, if any locator was an address.                          |
| `502`/`504` from the routing provider (upstream error, deadline) | **Yes**, not refunded — the call was placed.                 | **Yes**                                                          |
| `503 geocodio_distance_budget_exhausted`                         | **Yes**, not refunded — see below.                           | **Yes**                                                          |

One rule covers almost the whole table: **you pay for upstream work we
actually performed on your behalf.** A bad locator costs the one geocode it
took to discover it was bad, not the matrix it would have fed.

<Warning>
  **`503 geocodio_distance_budget_exhausted` is the one exception, and it is
  deliberate.** Our fleet-wide monthly routing budget being exhausted means no
  call reaches the provider — yet the driving part is still billed, because the
  debit is placed *before* the budget reservation on purpose. Reversing that
  order would let a caller who is over their own credit cap reserve, and waste,
  shared budget on every request. Since the error is retryable with
  `Retry-After: 3600`, **do not retry it in a tight loop** — each attempt is
  billed. Wait out the window.
</Warning>

## Errors

All errors use the standard `{"detail": {"error", "message", "retryable"}}`
shape (see [Errors](/api-reference/errors)).

| Error code                               | HTTP | Retryable | Meaning                                                                                                                                                                                                                                                                               |
| ---------------------------------------- | ---- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_request`                        | 422  | no        | The body doesn't match the schema — a bad field, an unknown `op`, or one of the [Limits](#limits) below. `detail.errors` carries pydantic's per-field detail.                                                                                                                         |
| `unresolvable_input`                     | 422  | no        | Every locator for a required role (an op's origin(s), or `screen`'s anchors) failed to resolve. Carries per-locator diagnostics — see below.                                                                                                                                          |
| `unknown_set`                            | 422  | no        | `nearest`'s `set` isn't one of the six curated sets.                                                                                                                                                                                                                                  |
| `shed_too_large`                         | 422  | no        | `labor_shed`'s annulus exceeded 3,000 tracts. Shrink `minutes`.                                                                                                                                                                                                                       |
| `proximity_request_exceeds_budget_share` | 422  | no        | This one request would reserve more than its allowed share of the shared monthly driving-matrix budget. The pool is **not** empty — the request is too big a bite of it, so retrying it unchanged will always fail. The message states the ceiling in driving calcs; split the batch. |
| `proximity_busy`                         | 429  | yes       | Per-worker overload gate. `Retry-After: 5`. Never billed — the gate is checked before metering.                                                                                                                                                                                       |
| `geocodio_distance_budget_exhausted`     | 503  | yes       | The shared, fleet-wide monthly driving-matrix budget is exhausted. `Retry-After: 3600`.                                                                                                                                                                                               |
| `proximity_data_unavailable`             | 503  | yes       | A backing local asset (a curated set, or the labor-shed tract table) isn't available right now.                                                                                                                                                                                       |
| `proximity_unconfigured`                 | 503  | no        | The service is missing its routing-provider credential. An operator problem, not a bad request.                                                                                                                                                                                       |
| `upstream_transient`                     | 502  | yes       | The routing provider had a transient failure (5xx, connection drop, rate limit).                                                                                                                                                                                                      |
| `upstream_auth`                          | 502  | no        | The routing provider rejected the credential or the request. Not your bug to fix by retrying.                                                                                                                                                                                         |
| `upstream_error`                         | 502  | yes       | Catch-all for a routing-provider failure the codes above don't name — a malformed distance-matrix response, or an undocumented status. Worth reporting if it persists.                                                                                                                |
| `proximity_deadline_exceeded`            | 504  | yes       | The request exceeded the endpoint's own deadline — distinct from a routing-provider timeout, which stays `upstream_transient`.                                                                                                                                                        |

### Diagnosing `unresolvable_input`

By far the most common way to hold this endpoint wrong is to name a *place*
rather than an address — `"detroit"`, or a bare street like
`"1820 meadowbrook circle"` (whose street *type* matches the town of Circle,
Montana). Those don't fail loudly at the geocoder: they match
something, just not a place specific enough to route from. The accuracy gate
refuses them rather than answering about the wrong location, and the `422`
tells you exactly what it matched so you can fix it in one edit:

```json theme={null}
{
  "detail": {
    "error": "unresolvable_input",
    "message": "every origin failed to resolve: 1 of 1 matched only to place-level accuracy, which is not specific enough to identify a location. supply a full street address (house number, street, city, state) or a 'lat,lng' coordinate — a bare street name, city, or place name is not specific enough, and a landmark's NAME is never resolvable (reach named infrastructure through a curated `set` or its own coordinate)",
    "retryable": false,
    "role": "origin",
    "unresolved_count": 1,
    "unresolved": [
      {
        "index": 0,
        "query": "1820 meadowbrook circle",
        "error": "low_confidence_resolution",
        "matched_address": "Circle, MT 59215",
        "accuracy_type": "place"
      }
    ],
    "caller_guidance": "This request could not be resolved as sent. You may retry with a more specific locator. If any part of that locator is something you INFERRED rather than something the caller supplied — a city, state, ZIP, or a coordinate substituted for a place name — you MUST say so explicitly when you report the result…"
  }
}
```

* **`role`** — which required role failed: `origin`, or `anchor` on `screen`.
  Only one role is reported; a request can fail on either.
* **`index`** — position in the list you sent, so you can map a failure back
  to the locator that caused it.
* **`error`** — `low_confidence_resolution` (matched, but too coarse to
  trust — the usual case), `unresolvable_input` (no match at all), or
  `geocoding_failed` (the lookup itself failed; worth a retry).
* **`matched_address`** / **`accuracy_type`** — what the geocoder actually
  landed on, and how precisely. No coordinate is echoed: the gate refused it,
  so returning it would invite you to use a point we don't trust.
* **`unresolved_count`** is exact; **`unresolved`** is capped at 10 entries so
  a bulk request returns a readable sample rather than one entry per locator.
* **`caller_guidance`** — the disclosure rule above, carried on the error so
  an agent reads it at the moment it's about to retry. On the MCP surface the
  same key appears alongside `unresolved_accuracy_types` (the tiers only; the
  addresses stay off that surface, see the note below).

Note that `matched_address` and `query` appear in the response body only —
never in `message`, which is retained in our request telemetry.

If the ambiguity is the *point* of your question — you have a name and want to
find out which place it means — use [`POST /v1/lookup`](/api-reference/lookup)
first. It returns ranked candidates and an explicit `clarify` disposition
instead of picking one, then feed the coordinate it gives you into
`/v1/proximity`. That's also the pattern that scales: resolving 40 candidate
sites once and computing over coordinates is cheaper and more predictable than
re-geocoding them on every call.

## Limits

* **Sync only.** No batch/async job endpoint for `/v1/proximity` in v1 — every
  request completes in the response.
* **Per-request caps:** `distance`/`screen` origins ≤ 500, `distance`
  destinations ≤ 500, `screen` anchors ≤ 10, `nearest` `n` ≤ 25,
  `labor_shed` `minutes` 5–90, annulus ≤ 3,000 tracts. On top of those,
  `distance`'s origins × destinations (and `screen`'s origins × anchors)
  must not exceed **10,000** — Geocodio's synchronous distance-matrix limit
  — and no more than **25 address-form locators** (see [Pricing](#pricing))
  may appear in one request. Both reject `422 invalid_request` in the standard
  error object — the breached cap and the advice to split are in `message`, and
  pydantic's per-field detail is in `errors`. These two are rejected before the
  handler runs, so they are **not billed** (see [Pricing](#pricing) for the
  ones that are).
* **Per-request share of the shared driving budget: 3,500 driving calcs.**
  Separate from the caps above and enforced against the *actual* upstream
  spend, after the 30-day memo has been consulted — so a request whose legs
  are already memoized can exceed 3,500 total calcs and still pass, because
  it buys nothing new. A request that would newly reserve more than that
  rejects `422 proximity_request_exceeds_budget_share` before any upstream call,
  naming the ceiling and how to shrink the request. It is never silently
  truncated, and it is **not billed** — the check runs before the debit (see
  the billing table under [Pricing](#pricing)). The ceiling clears
  `labor_shed`'s 3,000-tract annulus cap, so a shed at the documented maximum
  always fits. This exists so no single caller can drain the shared monthly
  budget and take `/v1/proximity` down for everyone else; the message is
  explicit that the pool still has room.
* **A driving leg is memoized for 30 days**, keyed on the rounded coordinate
  pair — a repeat lookup of the same origin/destination within the window is
  served from the memo and still bills at the same rate (the memo is margin,
  not a discount; pricing is charged from the request shape either way). A
  failed lookup is never memoized, so a transient outage can't freeze a wrong
  answer for a month.
* **Coverage is US + Canada, driving only** — every driving response says so
  in `notes`.


## OpenAPI

````yaml POST /v1/proximity
openapi: 3.1.0
info:
  title: Mireye Earth
  description: >-
    Provenance-tagged geospatial data for US coordinates. POST /v1/fetch for
    deterministic field values (POST /v1/fetch/batch for up to 25 locations at
    once); POST /v1/ask for natural-language Q&A; GET /v1/meta/fields for the
    catalog.
  version: 0.14.0
servers: []
security: []
paths:
  /v1/proximity:
    post:
      summary: >-
        Multi-coordinate drive-time compute: distance, nearest, screen, labor
        shed
      description: >-
        One discriminated-union body (`op`) covers four ops:


        - `distance` — N origins x M destinations, driving or straightline.

        - `nearest` — top-N candidates from a curated set (`@airports`,
        `@substations`, `@power_plants`, `@rail`, `@ports`, `@urban_areas`),
        ranked by driving duration.

        - `screen` — filter origins by driving-duration proximity to ANY of up
        to 10 anchors.

        - `labor_shed` — civilian labor force + population within a
        driving-minutes shed of one origin.


        Every response echoes `paid_driving_calcs` (what pricing charged for,
        computed from the request shape, never from how many locators happened
        to resolve) and `notes` (coverage + traffic honesty labels). A locator
        is a coordinate (`"lat,lng"`) or a street address; per-item resolution
        failures never fail the whole request (bulk destinations/candidates)
        except for a required role (an op's origin(s), or `screen`'s anchors),
        which 422s.


        **Send the most precise locator you have.** An underspecified one does
        not fail loudly upstream — it matches a real but WRONG place. `"1412
        market street"` matches a town called Market in West Virginia; `"SFO
        airport"` matches a town called Airport in North Carolina. Both are then
        refused rather than answered about the wrong place, and `422
        unresolvable_input` reports per locator what was matched instead and at
        what accuracy tier, so the fix is one edit. Include city+state or a ZIP
        on every address; use a coordinate or a curated `set` for named
        infrastructure. If your own caller supplied the ambiguous text and you
        fill in the missing part yourself, say so in what you report back — see
        `caller_guidance` on the error.


        Retryable failures (`503 geocodio_distance_budget_exhausted`, `503
        proximity_data_unavailable`, `502 upstream_transient`) carry a
        `Retry-After` header where applicable. `503 proximity_unconfigured`
        means the service is missing its Geocodio credential — an operator
        problem, not a bad request.
      operationId: compute_endpoint_v1_proximity_post
      requestBody:
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/DistanceOp'
                - $ref: '#/components/schemas/NearestOp'
                - $ref: '#/components/schemas/ScreenOp'
                - $ref: '#/components/schemas/LaborShedOp'
              title: Req
              discriminator:
                propertyName: op
                mapping:
                  distance:
                    $ref: '#/components/schemas/DistanceOp'
                  nearest:
                    $ref: '#/components/schemas/NearestOp'
                  screen:
                    $ref: '#/components/schemas/ScreenOp'
                  labor_shed:
                    $ref: '#/components/schemas/LaborShedOp'
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
components:
  schemas:
    DistanceOp:
      properties:
        op:
          type: string
          const: distance
          title: Op
        origins:
          items:
            type: string
          type: array
          maxItems: 500
          minItems: 1
          title: Origins
        destinations:
          items:
            type: string
          type: array
          maxItems: 500
          minItems: 1
          title: Destinations
        mode:
          type: string
          enum:
            - driving
            - straightline
          title: Mode
          default: driving
        units:
          type: string
          enum:
            - miles
            - km
          title: Units
          description: >-
            Reserved for a future single-unit rendering. `Leg` always returns
            BOTH distance_miles and distance_km regardless of this value -- no
            behavior currently depends on it.
          default: miles
        max_credits:
          anyOf:
            - type: integer
              maximum: 200000
              minimum: 1
            - type: 'null'
          title: Max Credits
          description: >-
            Refuse this request if it would cost more than this many credits.
            Defaults to the service ceiling (see
            MIREYE_PROXIMITY_CREDIT_CEILING); raise it to opt into an expensive
            request, lower it to cap your own spend. The refusal is a 422 that
            states the exact price, and it happens BEFORE the driving matrix is
            charged.
      type: object
      required:
        - op
        - origins
        - destinations
      title: DistanceOp
      description: N origins x M destinations, driving or straightline.
    NearestOp:
      properties:
        op:
          type: string
          const: nearest
          title: Op
        origin:
          type: string
          title: Origin
        set:
          type: string
          title: Set
        'n':
          type: integer
          maximum: 25
          minimum: 1
          title: 'N'
          default: 3
        filters:
          anyOf:
            - additionalProperties:
                anyOf:
                  - type: number
                  - type: string
              type: object
            - type: 'null'
          title: Filters
        mode:
          type: string
          enum:
            - driving
            - straightline
          title: Mode
          default: driving
        max_credits:
          anyOf:
            - type: integer
              maximum: 200000
              minimum: 1
            - type: 'null'
          title: Max Credits
          description: >-
            Refuse this request if it would cost more than this many credits.
            Defaults to the service ceiling (see
            MIREYE_PROXIMITY_CREDIT_CEILING); raise it to opt into an expensive
            request, lower it to cap your own spend. The refusal is a 422 that
            states the exact price, and it happens BEFORE the driving matrix is
            charged.
      type: object
      required:
        - op
        - origin
        - set
      title: NearestOp
      description: |-
        Top-N candidates from a curated set, ranked by driving duration (or
        straightline distance in ``straightline`` mode).
    ScreenOp:
      properties:
        op:
          type: string
          const: screen
          title: Op
        origins:
          items:
            type: string
          type: array
          maxItems: 500
          minItems: 1
          title: Origins
        anchors:
          items:
            type: string
          type: array
          maxItems: 10
          minItems: 1
          title: Anchors
        max_minutes:
          type: integer
          maximum: 300
          minimum: 1
          title: Max Minutes
        min_minutes:
          anyOf:
            - type: integer
            - type: 'null'
          title: Min Minutes
        max_credits:
          anyOf:
            - type: integer
              maximum: 200000
              minimum: 1
            - type: 'null'
          title: Max Credits
          description: >-
            Refuse this request if it would cost more than this many credits.
            Defaults to the service ceiling (see
            MIREYE_PROXIMITY_CREDIT_CEILING); raise it to opt into an expensive
            request, lower it to cap your own spend. The refusal is a 422 that
            states the exact price, and it happens BEFORE the driving matrix is
            charged.
      type: object
      required:
        - op
        - origins
        - anchors
        - max_minutes
      title: ScreenOp
      description: Filter origins by driving-duration proximity to ANY of up to 10 anchors.
    LaborShedOp:
      properties:
        op:
          type: string
          const: labor_shed
          title: Op
        origin:
          type: string
          title: Origin
        minutes:
          type: integer
          maximum: 90
          minimum: 5
          title: Minutes
        max_credits:
          anyOf:
            - type: integer
              maximum: 200000
              minimum: 1
            - type: 'null'
          title: Max Credits
          description: >-
            Refuse this request if it would cost more than this many credits.
            Defaults to the service ceiling (see
            MIREYE_PROXIMITY_CREDIT_CEILING); raise it to opt into an expensive
            request, lower it to cap your own spend. The refusal is a 422 that
            states the exact price, and it happens BEFORE the driving matrix is
            charged.
        estimate:
          type: boolean
          title: Estimate
          description: >-
            Price this shed without running or paying for it. The tract
            prefilter that determines the price is free, so the estimate is
            EXACT, not a guess. Returns a LaborShedEstimate; no driving matrix
            is called and no driving credits are charged.
          default: false
      type: object
      required:
        - op
        - origin
        - minutes
      title: LaborShedOp
      description: >-
        Civilian labor force + population within a driving-minutes shed of one
        origin.
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          type: array
          title: Detail
      type: object
      title: HTTPValidationError
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
              - type: string
              - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
        input:
          title: Input
        ctx:
          type: object
          title: Context
      type: object
      required:
        - loc
        - msg
        - type
      title: ValidationError

````