Skip to content

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"
GET /places/searchfind places by name
GET /places/nearbyplaces 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

ParameterRequiredMeaning
qyesNon-blank place-name query.
limitnoInteger 1–50, default 10. Invalid values return 400 bad_request.
langnoPreferred language for name, default en. names retains all available languages.
bboxnominlng,minlat,maxlng,maxlat. A hard filter: results outside it are excluded.

GET /places/nearby

ParameterRequiredMeaning
categoryyes1–8 comma-separated taxonomy ids or English/French aliases.
nearone anchorlng,lat. Send exactly one of near or bbox.
bboxone anchorminlng,minlat,maxlng,maxlat. Uses the centre and a radius covering the box, rather than clipping results to the rectangle.
radiusnoMetres, 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.
limitnoInteger 1–50, default 10. Invalid values return 400 bad_request.
langnoPreferred 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": null is 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