Documentation navigation

Boundary Search

A coordinate resolves to the administrative areas that contain it, each area has a boundary polygon, and every polygon can be used as a filter. One click on the map below exercises all three — and the three are priced differently, which is the part that surprises people.

Click anywhere on the map. Clicking open ocean is fine — it resolves to nothing, which is a real answer rather than an error.

What just happened

The demo above is the keyless, throttled twin of three ordinary API calls. With a key, the same click is these three requests:

  1. Point to area. GET /v1/resolve?lat=&lon= returns every administrative area whose boundary polygon covers the point, ordered country first. It returns nothing when no polygon covers the point, rather than the nearest thing it can find — that is what separates it from /v1/reverse. 1 quota unit, the standard request cost.
  2. Area to polygon. GET /v1/boundaries/{geoname_id} returns that area's boundary as GeoJSON, optionally thinned with ?simplify=. 5 quota units — the payload is two to three orders of magnitude larger than an ordinary list response.
  3. Polygon to the places inside it. GET /v1/cities?within=<geoname_id> returns only the rows falling geometrically inside that area. It works on /v1/cities and /v1/postal-codes, takes exactly one id, and a comma-separated list is a 422 rather than a silently used first value. 2 quota units.

The demo runs the first two. The third needs a key, so the panel above prints the request rather than issuing it.

What each call costs

Your plan's monthly allowance is counted in units, not in requests. Most requests cost one unit. These four do not:

  • GET /v1/boundaries/{geoname_id}5 units
  • ?within= on a list endpoint — 2 units
  • ?bbox= on a list endpoint — 1 unit
  • GET /v1/resolve1 unit

The highest multiplier wins; costs never add up

A request that qualifies for more than one multiplier is charged the largest of them, not the sum. A boundary fetch that also carries ?bbox= costs 5, not 6. Combining ?within= with ?bbox= costs 2, not 3 — and that one is not merely a rounding convention in your favour. A bounding box narrows the candidate set through an index before the exact point-in-polygon test runs, so the combined query is dramatically cheaper for us to serve than the containment test alone. You are not charged extra for making our work easier.

A rejected filter still costs throttle slots

Rate limiting and quota metering are two different meters. A ?within= request that is rejected — a malformed id, a city id where an area was required — has already taken 2 slots from your per-second allowance, because the cost is known before the request is validated. It meters 1 quota unit against your monthly allowance, because nothing was served. Getting a filter wrong in a tight loop shows up as 429s well before it shows up on the invoice.

What this means on the Free plan

Free includes 5,000 units per month. At 5 units each, that is 1,000 boundary fetches — not 5,000. It is 2,500 ?within= requests, or 5,000 resolves.

The per-second limit interacts with this in a way worth stating outright. A route's cost is clamped down to your plan's own per-second limit before it is charged against it, because an unclamped 5-unit route would be unreachable on a 2-per-second plan rather than merely expensive. The clamp hands Free one boundary fetch per second: reachable, still metered. Free's monthly allowance binds long before that rate does.

The full per-plan arithmetic is on the pricing page.

Coverage, and where it runs out

Country boundaries are close to complete: 235 of 236 parseable admin-0 records carry geometry (99.6%). Region boundaries are a different story, and the headline number hides most of it.

Of 52,840 geoBoundaries polygons considered, 45,138 matched a region row (85.4%). That produced 29,691 region rows carrying geoBoundaries geometry; with the 4,379 rows already carrying Natural Earth geometry, 34,070 regions have a polygon.

Check your countries before you build on this. The match rate is not evenly distributed and the per-country reality is what will decide whether this works for you. At ADM1, Spain landed 16 of 22 and the Philippines 16 of 18 — but Italy gained zero of 24, France 4 of 35, and Great Britain 2 of 18. A polygon we could not match to a region row unambiguously is refused rather than guessed, because a wrong match is served through ?within= as authoritative.

Coverage stops at countries and administrative regions. There is no promise of anything below the levels described here, and there is no endpoint that would serve one.

Containment performance

?within= does not test your point against one enormous polygon. Every area's boundary is pre-cut into tiles — 106,187 of them across the dataset — so the index prunes to a handful of small pieces before the exact test runs. The measured difference against the whole-polygon form on the same targets, in the same run:

  • United States — 19,862.0 ms p50 / 24,439.1 ms p95 against the whole polygon, versus 157.6 / 175.1 ms against the tiles. Roughly 140x.
  • Russia — 5,321.8 / 5,425.0 ms versus 115.3 / 138.8 ms. Roughly 39x.

Across the probed targets the tiled p95 runs from 22.7 ms (Northern Division, Fiji) to 175.1 ms (United States), with Canada at 52.9 ms and Bavaria at 37.6 ms.

There is no blanket latency guarantee here, and we are not going to invent one. The cost of a containment query tracks the number of rows that actually match, not the size or the tile count of the area — so a dense country with many matching cities is slower than a large empty one. China's containment query measures 590 to 628 ms against a 1.0 s statement threshold, about 1.6x of headroom. Query the areas you care about and measure them; do not extrapolate from the table above.

Changes

2026-08-30 — region geometry now requires a paid plan

Breaking change. Shipped to production on 2026-08-30.

Four doors led to region geometry. All four changed for Free keys, and three of them changed silently — still 200, with the geometry key simply absent and no error code anywhere on the path. If your integration reads data.geometry and started finding nothing there, this is why.

  • GET /v1/regions/{id}silent. Geometry was in the default response shape here, so this one breaks without your asking for anything new.
  • GET /v1/regions with ?fields= naming geometry — silent. Omitted from every row.
  • GET /v1/countries/{code}/regions with ?fields= naming geometry — silent. Omitted from every row.
  • GET /v1/boundaries/{geoname_id} for a region-level id — loud. A 403 carrying error code tier_upgrade_required and an upgrade_url in the body.

The two list routes are the larger half of this and the easier half to miss. GET /v1/regions?fields=geometry&limit=100 returned 9.5 MB of region GeoJSON for a single quota unit, while a single polygon through /v1/boundaries/{geoname_id} costs five. The list door was the cheaper door by two orders of magnitude.

GET /v1/countries/{code} is not affected. Country geometry is deliberately not gated and stays available on every plan including Free. That asymmetry is the shape of the offer, not an unfinished edge of it: Free gets country boundaries, and admin-level boundaries are what a paid plan buys.

Migration. Either:

  • Move to a paid plan — Basic, Pro or Business — which restores region geometry on all four routes. See pricing.
  • Or call GET /v1/boundaries/{geoname_id} with a country-level id, which serves country geometry on every plan including Free at 5 quota units per fetch.

Next

  • Plans and limits: Pricing — what each plan's monthly units buy once the multipliers above are applied.
  • First request: Getting Started — key, header, and a copy-paste call in four languages.
  • Where the polygons come from: Attribution — the boundary sources and the licence each one carries.