Places
Every request needs an API key; see Authentication.
curl "https://api.unmap.dev/places/nearby?category=bakery&near=-114.07,51.04&key=$UNMAP_API_KEY"const { data, meta } = await unmap.places.nearby("bakery", { near: [-114.07, 51.04] });GET /places/search | find places by name |
GET /places/nearby | places of a category near a point |
GET /places/{id} | one place, by an id you already hold |
/geocode/nearby is a separate endpoint over the same rows. Places is a first-class presentation of the POI
rows the geocoding corpus already carries, not a second store.
Coverage stays Canada. A missing q on /places/search is 400
{ "error": "q is required", "code": "bad_request" }. A bbox corner outside WGS84 is 400
{ "error": "longitude is -180..180 and latitude -90..90", "code": "bad_request" }. A box
outside Canada stays a filter: no match is an empty list, not outside_coverage. On
/places/nearby, that WGS84 check is the same 400, and an anchor outside Canada, or a bbox
whose centre is outside Canada, is 422
{ "error": "outside Canadian coverage", "code": "outside_coverage" }. The corpus is not queried.
Parameters
GET /places/search
| Parameter | Required | Meaning |
|---|---|---|
q | yes | Non-blank place-name query. |
limit | no | Integer 1–50, default 10. Invalid values return 400 bad_request. |
lang | no | Preferred language for name, default en. names retains all available languages. |
bbox | no | minlng,minlat,maxlng,maxlat. A hard filter: results outside it are excluded. |
GET /places/nearby
| Parameter | Required | Meaning |
|---|---|---|
category | yes | 1–8 comma-separated taxonomy ids or English/French aliases. |
near | one anchor | lng,lat. Send exactly one of near or bbox. |
bbox | one anchor | minlng,minlat,maxlng,maxlat. Uses the centre and a radius covering the box, rather than clipping results to the rectangle. |
radius | no | Metres, clamped to 50–50,000. Defaults to 5,000 with near, or the box-derived radius with bbox. An explicit radius overrides that derived value. |
limit | no | Integer 1–50, default 10. Invalid values return 400 bad_request. |
lang | no | Preferred language for name, default en. |
GET /places/{id}
id is the opaque place id returned by a Places response. Encode it as a path segment.
lang is optional, with default en. Returns { data: place, meta }; search and nearby
return { data: place[], meta }, including an empty array when no place matches.
See Response for the fields and Known gaps for id stability.
What /places/search adds over /geocode/search
It returns places. /geocode/search finds whatever best matches your query, which for "Calgary"
is the city itself; that is the right answer for a geocoder and the wrong one when you wanted
somewhere to go.
/places/{id} is new
The geocoder had no by-id lookup at all. If you stored a result, you had no way to refresh it. Now you do:
curl "https://api.unmap.dev/places/um:place:1234?key=$UNMAP_API_KEY"A malformed id is a 404 { "error": "not found", "code": "not_found" }, not a 400. An id
that parses and is not in the corpus is also 404 not_found. That message is
no such place, or the geocoding container predates /places/{id}. The status does not tell the
two cases apart.
The id is opaque, and it is stable within a corpus version rather than forever. Do not parse the tail, and do not treat it as a long-lived foreign key: a corpus rebuild may renumber the underlying row, so an id you stored months ago can stop resolving or, worse, resolve to a different place. Re-resolve by name and coordinate on a schedule, or store the coordinate and the name alongside the id so you can. A persistent place identity that survives a rebuild is not built yet; it is in Known gaps.
From the SDK
import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({ key: process.env.UNMAP_API_KEY! });
const { data, meta } = await unmap.places.nearby("boulangerie", { near: [-73.57, 45.5] });
await unmap.places.search("calgary tower");
await unmap.places.get("um:place:1234");meta comes back rather than being unwrapped away: without attribution you cannot comply with
the licence the data came under. Places is exported from @unmap/geocoding too, if you want it
without the rest of the SDK.
The fields we do not have
The response carries an id, names in every language the corpus holds, a category, coordinates and
an address. It does not carry phone, website, opening_hours, accessibility, operator
or a structured attributes bag.
Those fields are absent from the response, not present and null. The difference is deliberate:
"phone": nullis a claim about the place, and usually a false one.- An absent key is a claim about our coverage, and it is true.
The loaders do not extract them today. Filling them needs loader work and a full corpus rebuild. Coverage says the same thing.
Response
{
"data": [
{
"id": "um:place:1234",
"source_id": "1234",
"name": "RBC Royal Bank",
"names": { "en": "RBC Royal Bank", "fr": "Banque Royale" },
"category": "finance.bank",
"category_group": "finance",
"coordinates": [-114.0676, 51.0447],
"address": { "housenumber": "339", "street": "8 Avenue SW", "city": "Calgary", "region": "AB" },
"distance": 313,
"source": "places.osm-poi"
}
],
"meta": { "attribution": "(c) OpenStreetMap contributors", "status": "current" }
}distance appears on /places/nearby only. Absent address parts are omitted for the same reason
the absent top-level fields are.
Categories
The same 89-category taxonomy /geocode/nearby uses, with English and French aliases resolved at
the gateway, so ?category=boulangerie works from raw curl. See
Geocoding for the full table.
EV charging
Charging stations are a Places category, not a separate API:
curl "https://api.unmap.dev/places/nearby?category=ev+charger&near=-114.07,51.05&key=$UNMAP_API_KEY"transport.charging_station is the canonical id; ev charger and charging station are aliases.
Coverage is thin outside cities, and we measured it rather than guessing. On 2026-09-17, within 25 km of a downtown anchor: major cities hit the 50-result cap; Halifax returned 27; along the Trans-Canada, Banff 16, Swift Current 3, Kenora 2; Whitehorse 2; Yellowknife and Iqaluit zero.
Yellowknife returning nothing is more likely a missing record than a missing charger, and is the first thing we will check against an authoritative inventory. Iqaluit's zero may well be correct: it is off the road network entirely.
No network, connector type, power rating, port count, access or status. Same reason as the fields above: the corpus has no columns for them, so they are absent rather than null.
There is no authoritative Canadian station inventory behind this. NRCan's locator is not published as a dataset on open.canada.ca, and the federal funded-project datasets are lists of projects that received money rather than chargers that exist, so they are deliberately not used as a stand-in.
Known gaps
- No deduplication across sources. The corpus conflates localities, not businesses. A chain with an entry in two sources can appear twice, and a persistent place identity that survives that is not built.
- Coverage is OpenStreetMap's. Dense in cities, thin in rural Canada, and it varies street by street rather than by a published rule.
/places/{id}needs a current geocoding container. Against an older one it answers 404 and the body says the route may predate the deployment, rather than claiming the place is not there.
Found something wrong? Report it: POST /places/{id}/feedback takes a report
about one place.
Next steps
- Find nearby places runs this search live.
- Geocoding API answers from the same corpus when you have a name rather than a category.
- Errors for the failure contract.