Find places
find_placesSearch for named places around a location or inside a boundary, filtered by category, brand, and operating status to get relevant results.
Instructions
Find named places, either near a point or inside an area's boundary.
Three mutually exclusive modes:
- Point + radius: pass lat and lon (radius_m defaults to 1000m), or the
same center as `where` — a {"lat", "lon"} dict, a GERS id, or a
free-text place name ("Alamo Square, SF"), resolved here, so a named
search is one hop: no geocode()/resolve_place() call first. lat/lon
or where, not both, and neither with division_id/area. An id/name
`where` adds a compact "resolved": {"name", "id", "lat", "lon",
"matched_by"} (absent for lat/lon or a {lat,lon} where); an ambiguous
name returns {"error": "ambiguous_place", "candidates": [...]} rather
than picking one. Results are nearest-first around that center.
- Division polygon: pass division_id (a GERS division id, e.g. one from
an admin-hierarchy chain) instead of lat/lon. Results are every matching place whose
point falls inside that division's true boundary polygon — no radius
to guess, and no circle clipping a coastline or straddling a border.
Results are ordered by name (there's no reference point to rank
distance from).
- Area by name: pass area ("Palo Alto") to get the division-polygon mode
without first resolving the id yourself. The name is resolved with the
same ranking geocode/resolve_place use; the resolved division is
echoed back as "area" on the response so it's clear which one was
searched. A name matching several equally-ranked divisions returns
{"error": "ambiguous_area", "candidates": [...]} listing their
division_ids rather than silently picking one, and an unresolvable
name returns {"error": "not_found"} rather than an empty result that
would read as "this place has no coffee shops".
category matches Overture's taxonomy (e.g. 'coffee_shop', 'restaurant',
'grocery'); name is a substring match on the place name — both compose
with either mode. Results include operating_status ("in business" /
"permanently closed" / null when unknown) — a business-lifecycle
signal, NOT opening hours; this data has no open-now information —
and a compact trust_note calibrated from confidence and that status.
min_confidence (0.0-1.0) keeps only rows whose confidence score is at
least that value; out-of-range values return a bad_request error.
operating_status filters to a single status (see the schema's enum for
accepted relabeled/raw values); "permanently closed"/"closed" also
match Overture's separate "closed_permanently" raw value, since both
relabel the same way. Unrecognized values return a bad_request error.
brand is a substring match on the place's brand name (e.g. 'Starbucks').
Brand data is sparse — most independent businesses have no brand at all,
so brand=X narrows results down to that chain only; the absence of a
result does NOT mean "not a Starbucks", it may just mean brand isn't
populated for that place. has_website/has_phone filter on whether a
place has any website/phone entries at all (presence, not content) —
each result row carries brand (string or null) and has_website/has_phone
(booleans) so an agent can see why a place matched, but the full
websites/phones arrays are only returned by place_details.
Every filter above composes with either mode, and each is a silent
no-op (not an error) if the column it needs (confidence /
operating_status / brand / websites / phones) is absent from the active
dataset — see degraded_fields() on the response.
name has two fallback tiers (point + radius mode only, #373) for when
the literal substring search finds nothing: an alternate-spelling match
("Munich" -> a place named "München") then a typo-tolerant fuzzy match
("Startbucks" -> "Starbucks"). A row found either way carries
"matched_by": "alt_name" | "fuzzy" (absent on an ordinary match), and a
top-level "note" names the spelling actually matched.
Returns {"results": [...]}, plus truncated/omitted_count if the answer
didn't fit the token budget. Returns a structured {"error": "bad_request",
...} if no mode's inputs are given (or more than one is), {"error":
"not_found", ...} if division_id/area doesn't match any known division, or
a structured {"error": ...} if the upstream dataset is unavailable or
missing columns this tool depends on. If a category filter was given and
it matched nothing (in either mode), a non-fatal "note" field hints that
the category slug may be wrong and points at search_categories.
A truncated answer — either token-budget-trimmed or because more
matching rows exist beyond limit — also carries "cursor"; pass it back
unchanged, with every other argument identical, to fetch the next page.
cursor is only ever issued for a literal or ambiguity-free match: an
answer built from #373's alt-name/fuzzy name fallback never carries one
(those pools are small; ROADMAP §4.4). A cursor for a different query,
or one that's malformed, returns {"error": "bad_cursor", ...} naming
the mismatch; a cursor issued against an older Overture release is
honored anyway, against the current release, with a one-line "note"
that rows may have shifted.
detail picks how much of each row comes back (ROADMAP §4.5, roadmap
feature 5): "compact" (the DEFAULT) is {id, name, category, lat, lon,
distance_m, trust} — trust is a tier string ("strong"/"ok"/"weak"/
"unknown") derived from the same confidence/operating-status signals
as "full"'s trust_note, so the two can never disagree; the payload
also carries one "trust_legend" line explaining the tiers. compact
keeps lat/lon (unlike "ids") so a composed call feeding these rows
into a map-rendering tool still has coordinates under the default
tier. "ids" is just {id,
distance_m} — deliberately coordinate-free, the cheapest shape, for
chaining straight into a batch id lookup or place_details. "full" is
every field, unchanged from before this param existed, including
trust_note's prose. division-polygon rows have no distance_m at any
tier (no reference point to measure from) — the key is omitted, never
null; they still carry lat/lon at every tier but "ids". detail is
presentation only: it does not affect which rows match or their
order, is NOT part of a cursor's query identity, and a cursor issued
under one detail continues correctly under a different one.
Projection happens before the token budget is applied, so a smaller
detail tier fits more rows per answer — that's the point of a tier
smaller than "full". Unrecognized values return a bad_request error
naming the accepted ones.
categories (mutually exclusive with category — passing both is a
bad_request) runs a checklist of up to 5 slugs in ONE scan instead of
category's one slug, each matched with identical substring/prefix
semantics. group_by_category=False (the default) merges every
category's matches into one nearest-first list, same shape and cursor
pagination as a single category (categories, sorted, is part of the
cursor's query identity). group_by_category=True instead buckets the
answer as {"results": {category: [rows...]}}, up to `limit` rows PER
category — a category with zero matches is simply absent from the
dict. Grouped answers carry no cursor at all (each category is already
limit-bounded from a single scan; page a specific one further by
re-running with category=<that slug> instead). More than 5 slugs, or
categories together with category, both return a bad_request error;
group_by_category=True together with a non-null cursor is also a
bad_request (there is nothing to continue). The category-miss "note"
(see above) also covers categories: it fires when the scan matched
none of the requested slugs at all.
within = {"minutes", "mode"?, "of"?} (roadmap §4.2) keeps only results
truly reachable from `of` within `minutes` by street-graph `mode` —
the real graph, not a radius guess; radius_m is ignored when set. `of`
(a LocationRef) defaults to the search center — lat/lon or whatever
`where` resolved to; required in division_id/area mode. An id/name `of`
adds a "resolved" echo, unless `where` already claimed that key, in
which case `of`'s match is named in the note. The answer gets a short
reachability note. A cold graph returns
{"error": "needs_confirm", ...} — retry with confirm=true once the
user agrees to wait (5-25s).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| lat | No | ||
| lon | No | ||
| area | No | ||
| name | No | ||
| brand | No | ||
| limit | No | ||
| where | No | ||
| cursor | No | Continuation cursor from a previous truncated answer; valid for the same query on the same data release. | |
| detail | No | Row detail tier for find_places rows: 'ids' (id + distance_m only), 'compact' (id/name/category/lat/lon/distance_m/trust), or 'full' (every field, incl. trust_note prose). Default: compact. | |
| within | No | ||
| confirm | No | ||
| category | No | ||
| radius_m | No | ||
| has_phone | No | ||
| categories | No | ||
| division_id | No | ||
| has_website | No | ||
| min_confidence | No | ||
| operating_status | No | Business-lifecycle status filter (relabeled or raw Overture value, case-insensitive). Default: no filter. | |
| group_by_category | No |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||