Skip to main content
Glama

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
PLACEROOT_TOOLSNoComma-separated list of tool profiles to load, reducing schema overhead. See docs/REFERENCE.md for details.
PLACEROOT_RECREATION_LAYERNoSet to '0' to disable the recreation layer (which adds OSM-derived places data). Default is enabled.

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
find_placesA

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).
summarize_areaA

Summarize what's in an area: total places and top categories.

Give the center as lat/lon, or as `where` — a {"lat", "lon"} dict, a
GERS id, or a free-text place name — but not both (and not neither);
either way returns {"error": "bad_request"} naming the choice. A
`where` given as an id/name adds a compact "resolved": {"name", "id",
"lat", "lon", "matched_by"} to the answer; absent for lat/lon or a
{lat,lon} where.

Returns a structured {"error": ...} instead of raising if upstream is
unavailable or the dataset is missing columns this tool depends on.
place_detailsA

One place, in full: addresses, websites, phones, socials, brand, source attribution, GERS id, confidence, operating status, and a compact trust_note.

Resolve either by GERS id (the `id` field find_places and other tools
return) or by name + lat/lon (nearest name match within radius_m of
that point). Pass id, or pass name together with lat and lon — not
both. Long array fields (addresses, websites, phones, socials, sources)
are capped and never silently dropped: a truncated field carries a
matching "<field>_omitted_count". Returns {"error": "not_found", ...}
if nothing matches, or a structured {"error": ...} if the upstream
dataset is unavailable or missing columns this tool depends on.

When looking up by id, also pass near_lat/near_lon — the lat/lon from
the find_places (or other tool) row the id came from — so the lookup
can be narrowed to a ~50km box instead of scanning the whole dataset.
Ignored when resolving by name. Omitting it still works, just slower on
a cold, uncached id.

lang (#410) requests Overture's language-tagged name variant for this
place, when the data has one: `name` becomes the variant and
`name_primary` is added only when it differs. Default: the stored
`preferences()` lang, else the primary name unchanged. Never invented
or transliterated.
within_distanceA

Is the nearest place matching category/name within max_distance_m of (lat, lon)?

Give the center as lat/lon, or as `where` — a {"lat", "lon"} dict, a
GERS id, or a free-text place name — but not both (and not neither);
either way returns {"error": "bad_request"} naming the choice. A
`where` given as an id/name adds a compact "resolved": {"name", "id",
"lat", "lon", "matched_by"} to the answer; absent for lat/lon or a
{lat,lon} where.

max_distance_m is required and must be a positive number of meters — a
zero, negative, non-finite, or missing value returns {"error":
"bad_request"} rather than silently searching a 0m (or omitted from a
call entirely, in which case the schema itself rejects it before this
tool ever runs) window and answering a confident-looking "false".

Returns {"within": bool, "nearest": {...place row with id...} | None,
"distance_m": float | None}. nearest is None if nothing matches within
a search window capped at max_distance_m * 2 — a real match further out
than that isn't found (documented, not a bug: keeps the search bounded).
name is a literal substring match only — no alt-spelling or typo
fallback applies here, so a misspelled name is an honest "no match",
never a silent yes about a different name.
Returns a structured {"error": ...} if upstream is unavailable or the
dataset is missing columns this tool depends on.
distance_matrixA

Straight-line (great-circle) distance in meters between every origin and destination.

origins and destinations are each a list of LocationRefs — a {"lat":
..., "lon": ...} dict, a GERS id, or a free-text place name, mixed
freely — capped at 10 each (100 pairs max). This is a plain haversine
calculation, not a routed distance or travel time, so it's cheap but it
is NOT what Google/Mapbox distance-matrix APIs return: no roads, no
turns, no travel time. For "how far can I get in N minutes" use
isochrone() instead; for actual routed times/distances between several
points use travel_time_matrix().

An id/name that failed to resolve returns an indexed error
(origins[i]: ... or destinations[i]: ...) with candidates on ambiguity
— checked after the 10-point cap, so an over-cap list always fails on
the cap first. Any origin/destination given by id/name adds "resolved":
{"origins": [{"index", "name", "id", "lat", "lon", "matched_by"}, ...],
"destinations": [...]} covering just those entries; each side is
present only if it had a string entry, and the whole key is absent when
every point was already coordinates.

Returns {"elements": [{"origin_idx": 0, "dest_idx": 0, "distance_m":
812}, ...]}, flat and origin-major (all destinations for origin 0,
then origin 1, ...), budgeted like every other tool. Empty origins or
destinations returns {"elements": []}. Returns a structured {"error":
"bad_request", ...} instead of raising if either list exceeds 10
points or a point is missing/non-numeric lat or lon.
meeting_pointA

Where several people should meet, fairly: candidate venues ranked by equalized travel time, not geometric distance.

Fairness objective: minimize the MAXIMUM per-person travel time to the
venue ("no one gets screwed"), tie-broken by the smaller spread
(max - min across everyone), then by the smaller total. This is
deliberately not "minimize the average" — that objective can strand
one person with a long trip so two others get a short one.

origins is 2-5 points, each a {"lat": ..., "lon": ..., "mode": ...}
dict, a GERS id, or a free-text place name, mixed freely — mode is
"walk", "cycle", or "drive", defaulting to "walk" when omitted (a
string origin always gets the default mode; give a dict with "mode" to
pick otherwise), and can differ per person (e.g. one driving, one
walking). An id/name that failed to resolve returns an indexed error
(origins[i]: ...) with candidates on ambiguity. Any origin given by
id/name adds "resolved": [{"index", "name", "id", "lat", "lon",
"matched_by"}, ...] for just those origins; absent when every origin
was already coordinates. category optionally filters candidate venues
to an Overture taxonomy slug (e.g. 'coffee_shop'); a wrong or
unrecognized slug is a silent zero-match, not an error.

Method: a seed center is computed from each origin's implied
straight-line travel time (not raw distance, so a walking participant
pulls the center toward them more than a driving one at the same
distance), venues are searched for near that seed, and each
candidate's real per-person times come from routing.route() — the
exact routed number, not the seed's approximation. The total routed
(candidate, origin) fan-out is capped at 16 pairs, regardless of
`limit` — 8 candidates at 2 origins, down to 3 candidates at 5.

Returns {"center": {"lat", "lon"}, "candidates": [{"id", "name",
"category", "lat", "lon", "per_person": [{"origin_idx", "mode",
"travel_time_min", "distance_m"}, ...], "max_travel_time_min",
"spread_min"}, ...]}, ranked fairest-first, capped at `limit` (default
3, max 5). per_person entries carry origin_idx aligned to the input
origins list, one entry per origin — a candidate that can't be routed
from every origin (no street graph nearby, or genuinely disconnected)
is dropped from the ranking entirely rather than ranked on a partial,
unfair comparison. A per_person leg whose street graph hit its
internal size cap carries "truncated": true (as does its candidate,
and the answer carries a note) — that leg's time may be off. An empty
"candidates" list is a valid answer (nothing matched the category
nearby, or nothing routed from every origin) — it carries a "note"
explaining which, including when every pair was over the mode's
straight-line routing cap (try a faster mode).

confirm=true after the user agreed to wait for a first-time
street-graph build (about 5–25 seconds; see `route`). Without it, a
fan-out that would need a cold graph build returns {"error":
"needs_confirm"} instead of silently blocking. Omit confirm unless you
just asked and they said yes.

A non-empty result also carries "map" (#369) — a render-ready payload,
keyword-splattable straight into this server's map-rendering tool (its
keys are exactly that tool's keyword arguments): pins every origin, the
fairest candidate picked out by class, the rest, and the fairness seed
center, plus a one-line summary naming the fairest venue and its
numbers. Absent when "candidates" is empty.

Returns a structured {"error": "bad_request", ...} if origins has
fewer than 2 or more than 5 points, a point is missing/non-numeric
lat or lon, or a mode isn't walk/cycle/drive; {"error": "bad_request",
...} with the offending coordinate if lat/lon is out of range; or a
structured {"error": ...} if the upstream places or transportation
dataset is unavailable or missing columns this tool depends on.
travel_time_matrixA

Routed travel time + distance between every origin and destination, by mode.

origins and destinations are each a list of LocationRefs — a {"lat":
..., "lon": ...} dict, a GERS id, or a free-text place name, mixed
freely — capped at 5 each (25 pairs max). Unlike distance_matrix's plain
haversine, this is a real shortest-path search over Overture's open
street graph — roads, one-ways, and each mode's own speed model, the
same cost model route() uses for a single pair, one mode per call;
omit mode to use the stored preferences mode, else walk.

An id/name that failed to resolve returns an indexed error
(origins[i]: ... or destinations[i]: ...) with candidates on ambiguity
— checked after the 5-point cap. Any origin/destination given by
id/name adds "resolved": {"origins": [{"index", "name", "id", "lat",
"lon", "matched_by"}, ...], "destinations": [...]} covering just those
entries; each side present only if it had a string entry, absent when
every point was already coordinates.

Reuses a single cached street graph across every origin and
destination when every origin-destination pair fits the mode's
straight-line cap and the whole point set fits one extraction circle,
running one Dijkstra per origin against every destination at once
rather than a search per pair — for a same-city matrix this costs
about what a single isochrone does, not one route() call per pair.
When the points are too spread out for one shared graph, falls back to
a route() call per pair (up to 25).

Returns {"mode", "elements": [{"origin_idx", "dest_idx", "duration_min",
"distance_m"}, ...], "durations_note"}, flat and origin-major like
distance_matrix. durations_note says these are speed-model estimates
over the open street graph, not live traffic. An unroutable pair (off
the street network, or on a disconnected fragment of it) gets
{"duration_min": null, "distance_m": null, "note": "unroutable"}
instead of failing the whole call; if every pair in the matrix is
unroutable the response also carries a top-level "note" saying so.
If the street graph hit its size cap the response carries "truncated":
true plus a note — capped extractions may present reachable pairs as
unroutable. Empty origins or destinations returns {"elements": []}.

Returns a structured {"error": "bad_request", ...} instead of raising
if either list exceeds 5 points, a point is missing/non-numeric lat or
lon, or mode isn't walk/cycle/drive. If no street graph exists
anywhere near every point in the matrix, returns {"error":
"no_graph_nearby"} — the same top-level failure route() and
optimize_route() give when nothing in the area is on the mapped
network, rather than a matrix of nulls.
suggest_areasA

Where within reach: neighborhoods ranked by travel budget + amenities.

The inverse of every other area tool — instead of "describe this place",
"find me a place". anchors is 1-3 {"lat", "lon", "mode"?, "minutes"?}
points (mode: walk/cycle/drive, default from stored preferences;
minutes: default 15). requirements is 1-8 free-text amenity/character
strings, scored the same way as area_score.score_locality — "parks",
"groceries", "coffee shop" resolve against the Overture taxonomy;
a subjective phrase ("quiet streets", "safe neighborhood", "good
schools") comes back {"measurable": false} rather than a guessed score
(see "honesty" in the response).

Method: the same street-graph reach analysis behind PlaceRoot's other
travel-time tools computes each anchor's reachable shed; with more than
one anchor, the sheds are intersected (a candidate must be reachable
within EVERY anchor's own time budget, not just one — "office" and
"gym" both mean both). divisions.divisions_in_polygon (#348) partitions
the (intersected) shed into candidate neighborhoods/localities; each
candidate is scored against requirements the same way
area_score.score_locality (#349) does. Returns {"anchors": [...],
"results": [{"division_id", "name", "subtype", "overlap_fraction",
"lat", "lon", "travel": [{"anchor_idx", "mode", "minutes_budget",
"travel_time_min", "distance_m"} or {..., "note": "unroutable"/
"no_graph_nearby"/...}, ...], "requirements": [...], "overall_score",
"reason"}, ...], "honesty"}, ranked by overall_score (unmeasurable-only
candidates sort last, never dropped) then overlap_fraction, capped at
`limit` (1-10, default 5). division_id is a stable GERS id — chain a
result into admin_lookup or summarize_area for more detail without
re-running the search. No polygons in the response by default.

An empty "results" list is a valid answer (e.g. two anchors' sheds don't
overlap at all, or nothing in the reachable area is a neighborhood/
locality) with a "note" saying which. A per-anchor travel leg that can't
be routed (the polygon-approximated shed boundary occasionally includes
a point routing itself can't reach) gets "note" instead of a time,
without dropping the whole candidate.

confirm=true after the user agreed to wait for a first-time street-graph
build (about 5-25 seconds per anchor that needs one). Every anchor is
checked before any graph is built, so a fan-out never starts some
anchors and then stalls needing confirm on the next. Omit confirm
unless you just asked and they said yes.

Returns a structured {"error": "bad_request", ...} if anchors isn't 1-3
points, a point is missing/non-numeric lat, lon, or minutes, minutes is
not > 0, or a mode isn't walk/cycle/drive; likewise if requirements
isn't 1-8 non-empty strings. Propagates the same structured errors as
the underlying reach analysis (unsupported_mode, no_graph_nearby,
radius_too_large) and divisions_in_polygon/score_locality (upstream_unavailable,
schema_degraded) — a partial shortlist from a failed anchor or scan is
never returned.
compare_areasA

Compare 2-5 areas side by side: category mix, density, and what differs.

areas is a list of centers sharing one radius_m, each a {"lat": ...,
"lon": ...} dict, a GERS id, or a free-text place/area name, mixed
freely — a named area compares the same radius_m circle around its
resolved point as a coordinate would (not its actual boundary; that's a
later feature). An id/name that failed to resolve returns an indexed
error (areas[i]: ...) with candidates on ambiguity. Any area given by
id/name adds "resolved": [{"index": i, "name", "id", "lat", "lon",
"matched_by"}, ...] for just those areas; absent when every area was
already coordinates. Returns per-area total_places, place density per
km^2, and category_counts aligned across areas for the top ~10
categories by combined count, plus "differentiators" — those categories
ranked by how much they differ, relatively, between areas (the fastest
way to answer "how is area A different from area B"). Returns a
structured {"error": ...} if areas isn't 2-5 centers, or if upstream is
unavailable or the dataset is missing columns this tool depends on for
any area (a partial comparison is not returned).

priorities (optional, up to 6) turns the comparison into a scored
verdict: each entry is {"label": your own term for the criterion,
e.g. "competition"; "category": an Overture taxonomy slug, or
"__density__" for overall place density as a foot-traffic proxy;
"prefer": "more" | "fewer"; "weight": 0.1-5, default 1}. Each area's raw
measure per priority is that category's count (or density) within
radius_m — matched exactly against the category taxonomy (slug plus its
descendants, so "park" never counts parking garages) and counted
explicitly even for categories outside the top-10 alignment above; the
per-priority winner is whichever area is better on that raw measure (a
tie has no winner for that priority); each area's verdict score is the
weight-summed share of each priority normalized against the best area
(measure/max for "more", min/measure for "fewer" — the best area always
gets 1.0, and every area measuring 0 makes all shares 1.0), and the
highest score wins overall (a tie leaves winner_idx null). Adds (never
replaces) "verdict": {"winner_idx", "scores", "reasons" (one sentence
per priority), "margin", and a fixed "measured_note"} — the note,
always present when priorities are given, states plainly that these are
open-data place counts/density, never revenue, rent, actual foot
traffic, or demographics, and that "__density__" is only a proxy. If
the dataset's category columns are all degraded, count-based priorities
can't be measured and the verdict comes back with null winner_idx and
scores plus "degraded": true rather than a fabricated score. Returns
bad_request for more than 6 priorities or a malformed one (missing
label/category, an unrecognized prefer, or a non-numeric weight).

When priorities produced a verdict, the response also carries "map"
(#369) — a render-ready payload, keyword-splattable straight into this
server's map-rendering tool (its keys are exactly that tool's keyword
arguments): a pin per area (the winner picked out by class), a cheap
circle outline per area (radius_m, not a real boundary) labeled with
its score, and a one-line summary restating the winner. Absent when
priorities weren't given, or a verdict couldn't be scored.
admin_lookupA

Containing admin hierarchy for a point: neighborhood up to country.

Point-in-polygon against Overture's divisions theme. Returns {"chain":
[{"name": ..., "type": "locality", "id": ...}, ...]} smallest division
first (e.g. neighborhood, then locality, county, region, country) — an
empty chain means no division in the active dataset contains the
point, which is a valid answer for remote areas, not an error. Returns
a structured {"error": ...} if upstream is unavailable or the divisions
dataset is missing the geometry column this tool depends on.
changes_in_areaA

What's opened or closed around here since a past Overture release.

Use this for "what's new around here", "what's closed since spring",
or any question with a time dimension — every other tool here answers
against a single, current snapshot of the data; this is the only tool
that compares two.

Area, exactly one of:
- place: a free-text area name ("Palo Alto"), resolved with this
  server's usual free-text area matching (prominence-ranked, "City,
  ST" suffix aware). The resolved division is echoed back as "area"
  on the response. A name
  matching several equally-ranked divisions returns {"error":
  "ambiguous_area", "candidates": [...]} rather than silently picking
  one; an unresolvable name returns {"error": "not_found"}. If the
  resolved division is too large to diff (bigger than this tool's
  per-side degree cap — countries, large regions), returns a
  {"error": "bad_request"} naming the area and suggesting a smaller
  one (a neighborhood or district instead of a whole city/region) or
  an explicit bbox.
- min_lon/min_lat/max_lon/max_lat: an explicit bbox (all four
  together, or none) — for when the caller already has coordinates
  rather than a name. Same size cap as the named-area path.

category, when given, filters both releases' scans identically before
diffing — see diff_places; a place that changed OUT of the category
between releases reads as "disappeared" from this filtered view, which
is the correct reading of "restaurants that changed", not a bug.

from_release/to_release: Overture release strings (YYYY-MM-DD.N). Omit
both to diff the previous release against the ACTIVE one — to_release
defaults to release.resolve_release(), the same release every other
tool in this conversation queries (env pins included), and
from_release defaults to the newest listed release older than it: an
adjacent, recent window. Not the oldest release Overture still serves
— years-old releases are schema-drifted enough that most compared
columns NULL out and everything reads as "changed"; pass explicit
releases for a wider window, and check "degraded_fields" on the
response when you do. Pass both to pick a
specific window; passing only one is a {"error": "bad_request"}. When
the release listing is reachable, an explicit release not in it is
also a {"error": "bad_request"} rather than a silent typo'd diff; when
the listing itself is unreachable (network trouble), explicit releases
are tried directly instead — they may still resolve even though the
list couldn't be built.

If neither release is given and no listed release is older than the
active one (a listing failure, or a world with only one live release),
this returns a structured {"error": "upstream_unavailable"} naming
whatever releases WERE found — never an empty diff that would read as
"nothing changed here" when the real answer is "the window couldn't be
built".

limit caps how many ranked rows each of appeared/disappeared/changed
carries (default 8, hard cap 25 — the same per-answer row bound every
other tool here uses); "counts" and the "*_by_category" breakdowns are
never reduced by it.

Returns a compact digest, not the full diff: headline "counts"
(appeared/disappeared/changed/unchanged, always exact within the
scanned bound), a `limit`-capped ranked slice of each of
appeared/disappeared/changed (id, name, category, confidence, lat,
lon — changed rows carry old_name/new_name and
old_category/new_category instead of name/category), a small
per-bucket "*_by_category" breakdown computed over everything the
scans saw (same denominator as "counts", not the limit-capped slice),
the "releases" window actually used, "degraded_fields" when either
release's schema is missing a compared column (that field was NULL on
that side, so treat "changed" with suspicion), and
"truncated"/"omitted_count" when more exists than fits
(omitted_count sums omissions across the three lists).

Honest framing (issue #309): a "notes" list is attached whenever it
applies — a disappearance may be delisting or data cleanup, not a
closure; an appearance may be newly-mapped, not newly-opened. Neither
claim is inferable from this data alone, so the digest says so rather
than implying otherwise.

Returns a structured {"error": ...} if upstream is unavailable or the
active places dataset is missing the id/bbox columns this tool depends
on, for either release.
summarize_buildingsA

Summarize building footprints in an area: count, footprint area, height/floor coverage, mix.

From Overture's buildings theme (issue #23). Returns count,
total/mean footprint area in m^2, height_known_pct/num_floors_known_pct
(height and floor count are sparse in real Overture data — this reports
coverage rather than pretending every building has a value, with
mean_height_m/mean_num_floors alongside when any are known), and
top_subtypes/top_classes (top 10 each by count). Returns a structured
{"error": ...} if upstream is unavailable or the dataset is missing
geometry/bbox.
buildings_atA

Nearest building footprints to a point, nearest first.

From Overture's buildings theme (issue #23). Returns {"results": [{id
(GERS), subtype, class, footprint_area_m2, height_m, num_floors,
distance_m}, ...]}. No raw geometry by default (design rule: answers,
not data) — pass include_geometry=true to also get each row's
footprint as GeoJSON, simplified to a small per-row token cap (each row
then also carries geometry_max_deviation_m, reporting what was lost).
Returns a structured {"error": ...} if upstream is unavailable or the
dataset is missing geometry/bbox.
land_use_atA

What kind of land is this: land use and land cover classification at a point.

From Overture's base theme (issue #167) — PlaceRoot's first tool over
base, distinct from the place-search and area-summary tools (those cover
discrete POIs, not the land itself). Returns {"lat", "lon", "land_use":
{"subtype", "class", "name"} or null, "land_cover": {"subtype",
"class"} or null}. No raw geometry (design rule: answers, not data).

null for either field means no polygon of that type covers the point —
coverage is OSM-derived and patchy outside well-mapped cities, so this
is a common, valid answer for a rural or remote point, not an error.
When multiple polygons overlap (Overture nests them, e.g. a park inside
a residential parcel), the smallest/most specific one is returned and
a "note" flags that the pick was made among several valid candidates.
Returns a structured {"error": ...} if upstream is unavailable or a
base-theme dataset is missing geometry/bbox, and {"error":
"bad_request"} for a non-finite or out-of-range coordinate.
infrastructure_atA

Infrastructure near a point, nearest first: bridges, towers, piers — and street furniture.

From Overture's base theme (issue #179), type=infrastructure — the
built things that are neither buildings nor POIs. Read the data
honestly before trusting an answer: this layer is dominated by street
furniture (street_lamp, bench, waste_basket, bollard, kerb, crossing),
which outnumbers landmark infrastructure roughly 50:1 in a city
centre. An unfiltered query in a dense area returns lamps and benches
and says nothing about whether a bridge is nearby. To ask about
landmarks, filter: subtype/infra_class match Overture's `subtype` and
`class` columns (case-insensitive substring; infra_class is `class`
under a non-reserved name) — e.g. subtype="bridge", subtype="tower",
subtype="power", infra_class="pier".

Returns {"center", "radius_m", "results": [{"id", "subtype", "class",
"name", "distance_m"}, ...]}, plus "truncated": true, "total_in_range"
and an explanatory "note" whenever more features matched than were
returned. id is the GERS id, usable with other GERS-keyed tools. No raw
geometry (design rule: answers, not data).

Radius search, not containment: most infrastructure is linear or a
bare point, so "what's within radius_m" is the answerable question.
distance_m is measured to the closest point on the feature, not its
centroid — a bridge you are standing on reads ~0 m, not "distance to
the middle of the bridge". radius_m echoes the effective radius, which
may be lower than requested (large values are clamped).

An empty results list is a valid answer, not an error: base-theme
coverage is OSM-derived and patchy, and "no infrastructure within
500 m" is a real finding. Returns a structured {"error": ...} if
upstream is unavailable or the dataset is missing geometry/bbox, and
{"error": "bad_request"} for a non-finite or out-of-range coordinate.
water_nearA

Water near a point, nearest first: waterfront check, distance to river/canal/lake.

From Overture's base theme (issue #200), type=water — oceans, bays,
lakes, ponds, reservoirs, rivers, streams, canals, springs, pools.
Returns {"center", "radius_m", "in_range_count", "results": [{"name"
(when named), "subtype", "class", "distance_m", "is_salt"/
"is_intermittent" (only when true)}, ...]}, plus "truncated": true and
a "note" when more matched than were returned. No raw geometry.

distance_m is to the closest point on the feature, not its centroid —
a canal bank you are standing on reads ~0 m. Water gets dense (an
Amsterdam canal district puts hundreds of rows in a 500 m circle),
which is what in_range_count and the filters are for:
subtype/water_class match Overture's `subtype`/`class` columns
(case-insensitive substring; water_class is `class` under a
non-reserved name) — e.g. subtype="canal", subtype="river",
water_class="lake".

"on_water": true plus "water_body" means the point is *inside* a water
polygon — a lake, a reservoir, a river. For oceans and seas that is a
coarse signal: Overture cuts them into 1-degree tiles whose landward
edge covers dry coastal land, so a waterfront building reads as inside
the ocean. Those bodies are reported this way rather than as a bogus
0 m "nearest water" row, and no distance-to-coast is derived from them
(their tile boundaries include phantom cuts through open water), which
also means subtype="ocean" cannot return distance rows. Lakes and
rivers carry none of that: however large, they appear in results with
a real edge distance. The "note" says which case applies.

An empty results list is a valid answer: coverage is OSM-derived, and
"no water within 500 m" is a real finding about an arid or unmapped
place. radius_m echoes the effective radius (large values are
clamped). Returns a structured {"error": ...} if upstream is
unavailable or the dataset is missing geometry/bbox, and {"error":
"bad_request"} for a bad coordinate.
geocodeA

Free-text place name -> ranked candidate locations, from Overture divisions and places.

No Nominatim, no third-party geocoding API. Matches localities,
neighborhoods, regions, and countries by name (exact > prefix >
substring), falling back to named places if that doesn't fill `limit`.
Returns {"results": [{name, type, lat, lon, id (GERS), admin_context,
rank_score}, ...]}, budgeted like every other tool. Returns a structured
{"error": ...} instead of raising if the remote scan fails.

A query with no location context in it at all (a bare place name that
matches no division, e.g. "Blue Bottle Roastery") can't be bounded to a
region, so the places half of the search is skipped rather than
scanning the global places dataset — minutes, not seconds (#105). That
case comes back empty with a "note" saying so and what to do instead.

A misspelled name that matches no division literally ("Berekley", or
"Berekley, CA" — the region suffix is set aside first) gets one
close-spelling retry over the local divisions table (#215); those
results rank below any literal match, carry "matched_by": "fuzzy", and
come with a "note" naming the spelling they were corrected to.

A query that is entirely a postcode ("94110", "1011AB") is answered as
one (#223): one result per country whose address points carry that code,
with "type": "postcode", "country", "address_count" and a null "id" (a
postcode is not a GERS entity). Codes are shared across countries far
more often than not, so the alternates below the top row are real
ambiguity. The accompanying "note" carries the granularity caveat (a
Dutch code is a street block, a US ZIP a district) and the coverage
limits -- including the countries the addresses theme covers but that
carry no postcode values at all, which is why a valid postcode can still
come back empty.

Exonyms work too (#214): names are matched against Overture's ~100
localized alternates as well as its canonical one, so "Munich" answers
München and "Tokyo" answers 東京都. `name` is always the canonical
spelling; such rows carry an extra "matched_name" naming the alternate
that matched.

lang (#410) requests Overture's language-tagged name variant instead
of a division row's primary name, when the data has one for that row
and language: `name` becomes the variant and `name_primary` is added
only when it differs. Default: the stored `preferences()` lang, else
the primary name unchanged. Never invented or transliterated — only a
variant actually present in Overture's data is ever returned.
`PLACEROOT_HOME=<city/area>` (#406) sets a home region once at startup;
a bounded score bonus then nudges same-tier ambiguous namesakes (the
"which Springfield" case) toward it — a bias, never a filter, so a
distant result stays in the answer, just not first. Only when the bias
actually changed the top result does a "note" say so, e.g. "ranked
toward your configured home region (Seattle); pass a city/near hint to
override". No home configured -> no bias, no note, behavior unchanged.
geocode_batchA

Geocode up to 20 free-text queries in one call, one best match each.

Cuts N round-trips of geocode() into one and, more importantly,
shares ONE local divisions name table across the batch (#329) so a
two-name walk is not N cold S3 scans. For each query, keeps only
the top candidate.
Returns {"results": [{"query", "name", "type", "lat", "lon", "id"
(GERS), "rank_score"}, ...]}, one row per query, in input order — a
query with no match gets the standard error envelope {"query",
"error": "not_found", "detail"} instead, and does not fail the rest of
the batch. queries is capped at 20; a longer list returns a structured
{"error": ...} rather than truncating silently. Budgeted like every
other tool. Returns a structured {"error": ...} instead of raising if
the remote scan itself fails.
search_categoriesA

Free text -> valid Overture category slugs, for the category param the place-search and area-summary tools take.

Lookup only — no geo filtering, no upstream dataset dependency; matches
against a bundled snapshot of Overture's places taxonomy (pinned to
schema v1.9.0). Ranks exact slug match > slug prefix > slug substring >
a match on any taxonomy path segment, so close siblings like "cafe" vs
"coffee_shop" both surface rather than one silently winning. If the
whole query matches nothing, falls back to a lexical phrase-intent
match against a curated synonym lexicon (e.g. "fix my cracked phone
screen" -> mobile_phone_repair). Returns {"results": [{"slug", "path",
"confidence"}, ...]} — path is the root-to-leaf taxonomy (e.g.
["eat_and_drink", "cafe", "coffee_shop"]), confidence is 0-1 and
descending, budgeted like every other tool. An empty/whitespace query
returns {"results": []}. limit is clamped to 0-50, matching every
other tool's limit handling (out-of-range values are not an error).
resolve_placeA

Free-text place reference -> ranked, typed GERS ids to hold onto.

Turns something like "the Whole Foods on Lamar" or "Travis County" into
stable Overture ids: merges geocode()'s division matches (locality,
region, county, country, ...) with a name-filtered find_places search
(a business or POI), bbox-limited to near_lat/near_lon if given, else to
the ~20km vicinity of the top division match.

**Split the location out of the query, and pass `city`.** You know
things this server does not: that "san jose airport" means San Jose,
California, that the Eiffel Tower is in Paris, that a user asking about
"BASIS Silicon Valley" means Sunnyvale. This server knows only what
exists at which coordinates in the current Overture release. When the
location arrives inside one string, it has to guess which words are the
place — and it guesses from map data alone, where "san" names a division
in Henan and "palo" names one in Leyte. Given `city="San Jose, CA"` and
`query="airport"` there is nothing to guess.

A wrong hint costs a miss and a retry, never a wrong answer: `city`
only bounds where the search looks, and the returned rows still come
from the data. Pass `near_lat`/`near_lon` instead when you have real
coordinates — they are the strongest hint of all.

When nothing resolves for want of a location, the reply carries
`need: "location"` and a `retry_with` sketch rather than only prose,
so the second call can be made without parsing English.

Returns {"results": [{"id" (GERS), "kind": "division" | "place",
"name", "lat", "lon", "match": "exact" | "prefix" | "substring" |
"fuzzy", plus "admin_context" for a division or "category" for a
place}, ...]}, ranked by match tier then prominence ("fuzzy" — reached
by close spelling rather than by containing the query at all, #215 for
divisions and #373 for places — ranking below every literal match).
A place found through #373's alt-spelling/typo fallback additionally
carries "matched_by": "alt_name" | "fuzzy", and a top-level "note"
names the spelling actually matched. Budgeted like every other tool.
An unresolvable query returns {"results": []} — not an error. Returns a
structured {"error": ...} instead of raising if the remote scan fails
or the places dataset is missing columns this tool depends on.

lang (#410) requests Overture's language-tagged name variant, same as
geocode() — but only for "kind": "division" rows; "kind": "place" rows
(from find_places, out of scope for #410 this round) always carry
their primary name. Default: the stored `preferences()` lang, else the
primary name unchanged.
Division candidates come from geocode() (#406), so a configured
`PLACEROOT_HOME` nudges the same ambiguous-namesake ties this tool
merges from — see geocode()'s docstring. resolve_place does not add its
own disclosure note for that; its own ranking already leads with
distance to `near_lat`/`near_lon`/`city` when one is given.
resolve_place_batchA

Resolve up to 25 GERS ids to compact place rows in one call.

Collapses N place_details(id=...) round-trips into one: for each id,
resolves it via the same lookup place_details uses and keeps only a
compact row — {"gers_id", "name", "category", "lat", "lon"} — not the
full place_details payload (addresses, websites, phones, socials,
sources, brand, confidence, ...). Use place_details for full detail on
a single id. Results are returned in input order; an id that doesn't
resolve gets the standard error envelope {"gers_id", "error":
"not_found", "detail"} instead and does not fail the rest of the
batch. gers_ids is capped at 25; a longer list
returns a structured {"error": ...} rather than truncating silently.
An empty list returns {"results": []}. Budgeted like every other tool.
Returns a structured {"error": ...} instead of raising if the remote
scan fails or the places dataset is missing columns this tool depends
on.
gers_lookupA

Any GERS id -> what it is, across themes, plus its cheap cross-theme joins.

The reverse of every other tool: hand back an id one of them returned
(a place, a division, or a building) and get the entity it names —
{"id", "theme", "type", "name", "lat", "lon", "summary", "related"} —
without needing to know which theme it came from. summary carries a few
theme-specific fields (place: category, confidence, brand; division:
subtype, country, region; building: class, height, floors); related
carries the containing division, plus the building at the point when
the id is a place. Never geometry.

Also pass near_lat/near_lon — the lat/lon of the row the id came from —
whenever you have them: the lookup is an id scan across up to three
themes, and the hint narrows each one to a ~50km box instead of a
full-theme scan. Omitting it still works, just much slower on a cold id.
The hint *bounds* the search rather than merely ordering it: an id
outside the box comes back not_found with a note saying so, and the
exhaustive lookup is the same call without near_lat/near_lon. Pass a
hint you are sure of, or none at all.

Transportation segment/connector ids are not resolvable yet and come
back as not_found. Returns {"error": "not_found"} if no theme claims
the id, {"error": "bad_request"} for a malformed id (a GERS id is an
opaque token — 32 lowercase hex characters) or an out-of-range hint,
or a structured {"error": ...} if upstream is unavailable.
reverse_geocodeA

Point -> nearest address (street/number/postcode) and its containing division chain.

Degrades to a divisions-only result (source: "divisions_only", plus a
note) if the addresses theme is unreachable, missing, or has no nearby
coverage — addresses is Overture's newest, least complete theme, so
this is the expected degraded path. Returns a structured {"error": ...}
instead of raising if the remote scan fails outright.
address_atA

Nearest street addresses to a point, nearest first: number, street, unit, postcode.

The address-level counterpart to reverse_geocode (issue #188): where
that returns one collapsed hop plus the admin chain, this returns the
few doorways around the point with the attributes an address lookup
wants. Returns {"results": [{number, street, unit, postcode,
postal_city, address_levels, country, distance_m}, ...]}, capped at 5.
Optional attributes are omitted when the source has no value for them.

No id is returned: Overture documents address ids as not GERS-stable, so
unlike a place/division/building id there is no durable handle to hand
out. For a stable reference to what is at a coordinate, use
reverse_geocode and hold onto the division it names.

Coverage is the thing to read carefully. The addresses theme is
Overture's only alpha theme and covers 39 countries — no UK, Ireland,
India, China, Korea or Russia, no Africa or Middle East, and little of
Latin America outside Brazil, Mexico, Chile, Colombia and Uruguay. An
empty results list is a valid answer, never an error, and always carries
a "note" saying whether the country is outside the theme's coverage
entirely or is covered but had nothing within the search radius. That
country is the one whose division polygon contains the point, so the note
stays correct next to a border; if the lookup behind it cannot run, the
note says so rather than asserting anything about the data.

Returns a structured {"error": ...} if upstream is unavailable, if the
dataset is missing the bbox/street columns this depends on, or for an
out-of-range coordinate.
geocode_addressA

Street address -> coordinates: "1600 Amphitheatre Parkway, Mountain View".

The forward counterpart to address_at, and finer than geocode, which
answers at city/neighborhood granularity and never at a doorway. The
first comma splits the street from the city; a bare integer at either end
of the street part is the house number ("1600 Amphitheatre Parkway",
"Hauptstraße 5"). Pass `number`/`street`/`city` instead if you already
have the parts. Unit/apartment numbers are not parsed.

The city is resolved first and its boundary bounds the search, so a city
that resolves to no boundary — or to something far larger than a city,
like a state — returns an empty list plus a note rather than a scan. If a
same-named runner-up in the same country supplies the boundary instead,
the note names it — the answer is never silently about a different city,
and never about one in another country. Check `anchor` (name, country,
admin_context) to see which one it was. Street names match in either
spelling (Parkway/Pkwy, West/W, NW/Northwest).

Returns {"results": [{number, street, unit, postcode, country,
distance_m, lat, lon}, ...], "anchor": {name, id, country,
admin_context}, "match": "exact"|"nearest_number"|"street"},
deduplicated to distinct number+street+postcode and nearest the city's
own point first. More matches than `limit` adds "truncated",
"distinct_in_range" and a note. `match` is absent only when no street
was scanned at all (no street name, no city, or an unresolved anchor).

A requested number with no address point is never interpolated: when the
street has other numbered points, `results` holds the real nearest known
numbers bracketing the miss instead (`match: "nearest_number"`, each row
its own genuine coordinates, plus a note naming the miss and neighbors)
— never a synthesized coordinate for the missing number. No usable
numbers on the street falls to `match: "street"`, today's empty-plus-note.

Coverage is alpha: 39 countries, no UK, Ireland, India or China. An empty
list is a valid answer and always carries a note saying whether the
country is uncovered or the street simply wasn't found.
reverse_geocode_batchA

Reverse-geocode many points in one call, to cut N round-trips down to one.

Accepts at most 20 points; a longer list returns a structured
{"error": "bad_request"} instead of processing anything. Returns one
row per point in `points`, in the same order — each row is whatever
reverse_geocode(lat, lon) returns (address/divisions chain, or a
"divisions_only" degrade — see reverse_geocode's docstring). A
malformed point (missing/non-numeric lat/lon, or a lat/lon out of
range) doesn't fail the whole batch — it yields the standard error
envelope {"lat", "lon", "error": "bad_request", "detail"} in its slot
instead.
simplify_geometryA

Simplify a GeoJSON geometry to fit a token budget, reporting what was lost.

Works on caller-supplied GeoJSON (Polygon, MultiPolygon, LineString,
MultiLineString; Points/MultiPoints pass through unchanged). Binary
searches the simplification tolerance until the result fits max_tokens
instead of asking the caller to guess one. Returns {"geometry": ...,
"max_deviation_m": ..., "original_points": N, "kept_points": M}, or a
structured {"error": "invalid_geometry", ...} for malformed input.
geometry_opA

Geometry math and predicates — one tool, many ops, no Overture scan.

`op` selects the operation; pass only the params it needs (points are
`{"lat": ..., "lon": ...}`; `geometry` is a GeoJSON object):

- `distance(point, point2)` -> `{"distance_m"}` (great-circle haversine distance)
- `bearing(point, point2)` -> `{"bearing_deg"}` (initial compass bearing)
- `destination(point, bearing_deg, distance_m)` -> `{"point"}`
- `midpoint(point, point2)` -> `{"point"}` (great-circle midpoint)
- `area(geometry)` -> `{"area_m2", "area_km2"}` (Polygon/MultiPolygon)
- `length(geometry)` -> `{"length_m"}` (LineString/MultiLineString)
- `bbox(geometry)` -> `{"bbox": [xmin, ymin, xmax, ymax]}` (any geometry)
- `centroid(geometry)` -> `{"point"}` (any geometry)
- `buffer(point, radius_m)` -> `{"geometry"}` (Polygon, ~32-vertex circle approximation)
- `convex_hull(points)` -> `{"geometry"}` (Polygon; points capped at 100)
- `point_in_polygon(points, geometry)` -> `{"results": [bool, ...]}` (Polygon/MultiPolygon,
  holes honored; points capped at 100)
- `nearest_point(point, points)` -> `{"index", "distance_m"}` (points capped at 100)
- `nearest_point_on_line(point, geometry)` -> `{"point", "distance_m", "fraction"}` (LineString)
- `union(geometry, geometry2)` -> `{"geometry", "area_km2"}` (Polygon/MultiPolygon, either slot)
- `intersect(geometry, geometry2)` -> `{"geometry", "area_km2"}`, or `{"empty": true, "note"}`
  when the two inputs don't overlap
- `difference(geometry, geometry2)` -> `{"geometry", "area_km2"}` (geometry minus geometry2),
  or `{"empty": true, "note"}` when geometry2 fully covers geometry

`buffer`, `convex_hull`, and `union`/`intersect`/`difference` are the
ops that return geometry; that output is simplified to fit the same
token budget `simplify_geometry`'s own default targets, so there's no
need to chain a second call. `union`/`intersect`/`difference` run via
the DuckDB spatial extension already loaded for other tools (see
geometry_setops.py) rather than geometry_ops.py's pure-Python math.

An unknown op returns `{"error": "bad_request", ...}` listing valid ops.
Missing/wrong-shaped params for the given op return `{"error":
"bad_request", ...}` naming exactly what that op needs, e.g. "op=buffer
needs point and radius_m". Point-like inputs are range-checked (lat in
[-90, 90], lon in [-180, 180]); `geometry`/`geometry2` get structural
validation only (right type, non-empty numeric coordinates) — see
geometry_ops.py's module docstring for the accuracy notes behind
area/centroid (a local meters projection, not a geodesic computation)
and buffer/convex_hull (planar approximations, fine at city/regional
scale).
render_mapA

Render any result as a shareable one-pager: map, verdict, and stop list.

Writes ONE self-contained HTML file — interactive SVG map (inline CSS/JS,
vector markers with labels and click popups, polygon/line shapes
including reachability output shaped {"polygon": ..., "stats": {...}}),
a composed verdict, per-stop details, a scale bar, and required
attribution. A shape feature's properties may carry "role": "shed"
(soft translucent fill, dashed edge — for travel-time sheds) or
"role": "outline" (no fill, strong edge — for a compared-area boundary);
any other/absent role keeps the default style. Properties may also carry
a short "label" and one-line "callout", rendered as a text chip over the
shape (capped ~40/~80 chars); for the reachability payload, set
role/label/callout at the payload's top level. No CDN, no tile server,
no API key, zero
network requests when opened — a local file the user can send as-is.
Pass `summary` for
the verdict you want on the page (the sentence you'd tell a spouse,
co-founder, or landlord); when omitted a short fallback is composed
from the payload. Written to PLACEROOT_ARTIFACT_DIR (default: alongside
the tile cache directory). The file itself is the artifact; this tool's
response stays small on purpose. Returns {"path", "bytes",
"features_rendered", "skipped_features"} (plus "truncated": True when
applicable) — skipped_features counts rows/features that couldn't be
rendered (missing coordinates, malformed geometry, or dropped past
mapview.MAX_RENDER_VERTICES) rather than failing the call outright. Pass
inline=true to also get the HTML back in the response when it's small
enough to be worth it.

A point in `result` carrying a "class" property gets a contrasting
marker dot when `legend` maps that class to {"label": str, "color":
str?} — pass e.g. {"open": {"label": "Open now"}, "closed": {"label":
"Closed", "color": "#d55e00"}}. A missing color is assigned from a
fixed color-blind-safe palette; an invalid one (not #rgb/#rrggbb hex)
is dropped rather than used. Classes actually present get a legend box
on the page; a class not in `legend` keeps the default dot and is
reported in the response's "note". Omitting `legend` (or a result with
no "class" properties) renders exactly as before.
isochroneA

Isochrone: the area reachable from a point within minutes, by mode.

Give the point as lat/lon, or as `where` — a {"lat", "lon"} dict, a
GERS id, or a free-text place name — but not both (and not neither);
either way returns {"error": "bad_request"} naming the choice. A
`where` given as an id/name adds a compact "resolved": {"name", "id",
"lat", "lon", "matched_by"} to the answer; absent for lat/lon or a
{lat,lon} where.

Builds a street graph from Overture's transportation theme and runs
Dijkstra out to the time budget. Each mode
excludes its own set of unusable road classes (e.g.
drive excludes footway/path/steps; cycle and drive exclude
motorway/trunk... drive itself allows motorways) and respects one-way
restrictions for cycle/drive (walk ignores them). speed_m_s overrides
the mode's default speed model (walk 1.4 m/s, cycle 4.2 m/s, drive
per-edge from Overture's speed_limits or a class-based default table)
with a single constant. Returns {"polygon": <GeoJSON Polygon>, "stats":
{reachable_nodes, max_radius_m, area_km2}, ...}. The polygon traces the
boundary of reached nodes' occupied grid cells (falling back to a
convex hull for very small reachable sets); reachable_nodes/
max_radius_m are always exact, only the drawn polygon shape
approximates, and is decimated/simplified to fit the token budget.

radius_m optionally overrides the auto-derived graph extraction radius
(capped per mode: 5km walk, 15km cycle, 60km drive); passing something
larger than the cap returns a structured error instead of silently
truncating. An unrecognized mode string returns a structured
{"error": "unsupported_mode"}. minutes must be > 0 and radius_m (if
given) must be >= 0, else returns {"error": "bad_request"}.
routeA

Route: shortest-path distance and duration between two points, by mode.

Give the two ends as from_lat/from_lon and to_lat/to_lon, or as
from/to — each a {"lat", "lon"} dict, a GERS id, or a free-text place
name — but not both (and not neither); either way returns
{"error": "bad_request"} naming the choice. Do not call geocode(),
resolve_place(), or geocode_batch() first: names and ids resolve in
parallel inside this call, and the "from"/"to" blocks come back
carrying whatever each end resolved to.

Compact directions, not turn-by-turn: builds a street graph from
Overture's transportation theme around the two points and returns

{"distance_m", "duration_s", "mode", "from", "to", "export"} for the fastest path — no polyline unless you ask for one. export is the pocket handoff: Google/Apple Maps directions URLs built from the same two coordinates (URL schemes only — no Maps API, no extra network), a GPX 1.1 document, and a printable stop list. Same cost model every routing tool uses (walk 1.4 m/s, cycle 4.2 m/s, drive per-edge from Overture's speed_limits or a class-based default table). drive's duration is a posted-speed model with no live traffic; all modes snap each endpoint to the nearest usable street-graph node (real routes rarely start/end exactly on a segment).

Each mode has a straight-line-distance cap on the two points, rejected
before any graph is built (see routing.ROUTE_MAX_STRAIGHT_LINE_M, derived
per-mode from the shared graph-extraction radius cap — roughly
walk 7.5km, cycle 23.5km, drive 95.5km) — real road distance only ever
exceeds straight-line, so anything past the cap can't produce a route
worth extracting for anyway; returns {"error": "route_too_long"} with
the exact cap in "max_distance_m". An unrecognized mode string returns
{"error": "unsupported_mode"}; non-finite or out-of-range coordinates
(lat outside [-90, 90], lon outside [-180, 180]) return
{"error": "bad_request"}. If no usable graph or street node is found
near either point, returns {"error": "no_graph_nearby"}. If both points
snap into the graph but no path connects them (e.g. disconnected
islands of road data), returns {"error": "no_route", "try": ...}
rather than raising — "try" is a mode-tuned next move (roadmap §4). If
the extraction graph hit its internal size cap, the result
carries "truncated": true — the route may be suboptimal or incomplete.

include_path=true adds "path", a GeoJSON LineString from the origin's
snapped node to the destination's that follows the streets' own
geometry (curves included), simplified to fit the token budget, with
"path_max_deviation_m" bounding how far it strays from the exact
street path. Off by default (the polyline dwarfs the rest of the
answer) — ask for it only to draw or trace the route. If even a fully
simplified line won't fit, you get "path_omitted": true instead of a
line that stops short of the destination.

include_elevation=true adds "elevation": a compact climb profile from
the same keyless Copernicus GLO-30 DEM reader as point elevation lookups
use, sampled along the route —
"total_climb_m", "total_descent_m", "max_grade_pct", and a small
"samples" array of [distance_along_m, elevation_m] points, thinned to
fit the token budget. Off by default. Where the DEM has no coverage
along part or all of the route, the affected numbers are never faked as
0.0 — you get a "note" saying so instead (and no climb/descent/grade
keys at all if there's no coverage anywhere on the route). If even the
note-only form can't fit the budget, you get "elevation_omitted": true.

prefer="flat" asks the router to trade distance for climb — steeper
detours cost more than gentler ones, so a longer-but-gentler path can
win over a shorter-but-steeper one. Only meaningful for mode="walk" or
"cycle" (returns {"error": "bad_request"} for mode="drive"); needs
per-node elevations for the extracted street graph, fetched from the
same Copernicus source (bounded — see routing.FLAT_MAX_ELEVATION_NODES),
so if that data isn't reachable or has no coverage here, the route falls
back to plain-distance routing and says so in "prefer_note" rather than
silently ignoring the preference. IMPORTANT: prefer="flat" minimizes
elevation grade only — it is NOT a step-free, stroller-, or
wheelchair-accessible mode. Overture's transportation data (as read
here) carries no step-count, kerb-ramp, or surface attributes, so a
flight of stairs classified as ordinary walkway geometry can still
appear on a "flat" route if it's short and roughly level. Don't offer
this as an accessibility guarantee to the user; it isn't one.

avoid=["motorway"] (and/or "trunk") is the "no highways" ask: those
classes and their on/off ramps are dropped from the street graph before
the search, and the answer echoes "avoid". Those two values are the
whole vocabulary — anything else is a bad_request listing them. There is
no toll or ferry option, deliberately: Overture's road data carries no
toll attribute at all, and this graph is road-only so ferries are never
routed over. Tell the user that rather than approximating either with
avoid=["motorway"]. On walk and cycle it is a no-op (both already
exclude those classes) and says so in "avoid_note" instead of erroring.
An avoiding route is a different graph, so the first one in an area can
need its own confirm even where a plain route is warm; if the avoided
roads were the only link, the usual no_route comes back with "try"
naming avoid.

confirm=true after the user agreed to wait for a first-time street-graph
build (about 5–25 seconds). Pass it only after a needs_confirm reply
and they said yes. A warm or cached graph never needs it.
Omit confirm unless you just asked and they said yes.

A from/to name matching several equally-ranked places returns
{"error": "ambiguous_place", "candidates": [...]} instead of picking a
city; an unresolvable name or id returns {"error": "not_found"}; a
malformed one (empty string, dict missing lat/lon, wrong type) returns
{"error": "bad_request"} — the offending side is named in "field".
Ends that resolve a city apart return {"error": "too_far"} with both
ends and the mode cap, before any graph is built. from_to is this same
routing with a walk default.
elevation_atA

Ground elevation in meters at a point, from Copernicus GLO-30 (~30 m resolution).

Reads the Copernicus DEM directly from AWS Open Data (no API key, no
third-party elevation service) — the same open-data pattern every other
tool here uses, just a different bucket than Overture's. Nearest-cell
sampling, not interpolated: at ~30 m ground resolution the answer is
"the elevation of the DEM cell containing this point", which can be off
by a few meters from the exact spot on a steep slope.

Returns {"elevation_m": <float>}. No coverage at this point — open
ocean, or a tile the Copernicus release excludes from public
distribution — is a real, non-error answer: {"elevation_m": null,
"note": "..."} explaining why. Returns a structured {"error": ...} for
an out-of-range coordinate, or if the DEM tile can't be fetched
(network/upstream failure).

Attribution: Copernicus DEM © DLR/ESA, accessed via AWS Open Data.
from_toA

Shortest-path walk, cycle, or drive between two places.

from_to is route() with LocationRef ends and a walk default; route is
the canonical routing tool and is growing the same from/to ends, so
prefer route(from=..., to=...) once it takes them.

Pass each of from/to as a free-text place name, a {"lat", "lon"} dict,
or a GERS id — mixed freely. Do not call geocode(), resolve_place(), or
geocode_batch() first. Plain names resolve in parallel exactly as
before; coordinates pass through untouched. Builds one street graph and
returns distance, duration, export maps/gpx/text, and a "from"/"to"
block carrying whatever the input resolved to (name/id when it was a
name or GERS id, lat/lon always).

A comma qualifies: "Alamo Square, SF" searches inside SF only.

If a name matches several equally-ranked places, returns
{"error": "ambiguous_place", "candidates": [...]} instead of picking
a city. If the two ends resolve a city apart, returns
{"error": "too_far"} with the resolved ends and the mode cap rather
than extracting a continent graph. Same per-mode straight-line caps
as a coordinate route (walk ~7.5 km, cycle ~23.5 km, drive ~95.5 km).
An unresolvable name or GERS id returns {"error": "not_found"}; a
malformed from/to (empty string, dict missing lat/lon, wrong type)
returns {"error": "bad_request"} — either way the offending side is
named in "field": "from" | "to". Omit mode to use the stored
preferences mode, else walk.

include_path, include_elevation, prefer, and avoid pass straight through
to route() — see that tool's docstring for what each returns/means
("elevation" climb profile, prefer="flat" grade-avoiding preference and
its honest step-free/accessibility caveats, avoid=["motorway"|"trunk"]
class avoidance and why no toll or ferry option exists). avoid needs
mode="drive": the walk default already excludes those classes.

confirm=true after the user agreed to wait for a first-time street-graph
build (about 5–25 seconds). Pass it only after a needs_confirm reply
and they said yes. A warm or cached graph never needs it.
Omit confirm unless you just asked and they said yes.
find_nearA

Places of a category near a named place or city.

Prefer find_places(where=..., category=...): it is the canonical form of
this search — the same one hop, plus every find_places filter, detail
tier, and mode. find_near stays as a thin alias.

Pass the user's place name as near. Do not call geocode(),
resolve_place(), or geocode_batch() first. One hop for a category
near a named landmark. Resolves near, then searches like a point
find. Returns compact rows (name, category, distance, trust_note)
plus the resolved near (name and coordinates).

A comma qualifies: "Le Marais, Paris" searches inside Paris only.

If near matches several equally-ranked places, returns
{"error": "ambiguous_place", "candidates": [...]} instead of picking
a city. An unresolvable name returns {"error": "not_found"}; empty
category or near returns {"error": "bad_request"}. radius_m and
limit follow the same clamps as a point search.

A truncated answer carries "cursor" (delegated straight through from
find_places); pass it back with the same category/near/radius_m/limit
to continue. See find_places' docstring for the bad_cursor/release-
mismatch details — they apply here unchanged.
ground_locationA

One-hop location grounding: where, surroundings, reach, notable.

Answers "orient me at this point" in a single call instead of chaining
a reverse lookup, an area summary, a reachable-area scan, and a
nearby-places search. Give the point as lat/lon, or as `where` — a
{"lat", "lon"} dict, a GERS id, or a free-text place name — but not
both (and not neither); either way returns {"error": "bad_request"}
naming the choice. A `where` given as an id/name adds a compact
"resolved": {"name", "id", "lat", "lon", "matched_by"} to the answer
(a separate key from the answer's own "where" section below); absent
for lat/lon or a {lat,lon} where. Returns:
- where: reverse_geocode's answer for the point (address/divisions
  chain, or a "divisions_only" degrade).
- surroundings: total places and the top few categories within a fixed
  500m radius, plus density_per_km2.
- reach: reachable-area stats only for (minutes, mode) —
  {reachable_nodes, max_radius_m, area_km2}. Never includes the
  reachable-area polygon; this tool returns no geometry, ever.
- notable: the nearest 2-3 named places, no category filter.

Each section is independent: if its underlying call fails or comes
back empty, that section is dropped and a short line explaining why is
added to "notes" instead — the call only fails outright if every
section failed, returning a structured {"error":
"upstream_unavailable", ...}.

minutes must be > 0 and <= 60; omit mode to use the stored
preferences mode, else walk.
Both, plus out-of-range coordinates, return {"error": "bad_request"}.
No confirm gate: the reach scan runs with the requested minutes/mode
as-is (it self-caps its graph extraction radius; no nearby street
graph just degrades the reach section to a note).
places_along_routeA

Places on the way from A to B: corridor search along the route.

Answers "find a coffee shop on my drive to the airport" — the route tool
plus find_places in one call. Builds the same street-graph shortest path
`route` returns, then finds places whose nearest point on that path is
within max_detour_m (default 1000m, capped at 5000m; larger values
return a bad_request error rather than being silently clamped).

Each result row is a find_places row plus two numbers: detour_m, the
straight-line distance to the route doubled — an approximation of the
round trip off and back on, not a re-routed detour — and along_m, how
far along the route from the origin that place sits, so "roughly
halfway" is answerable. Results are ordered by along_m (route order,
reading as an itinerary) rather than by detour cost. When more than
limit places are on the way, the response is an even sample spanning the
whole route — never just the first limit, which would drop the far end
of the journey — and carries "truncated": true saying so. It also
carries {"route": {"distance_m", "duration_s", "mode"}} for the
underlying route. A composed itinerary also carries
verify_before_going when any stop is low-confidence or listed closed,
naming the 1–2 places most worth checking.

category and name narrow the search exactly as they do in find_places
(category matches Overture's taxonomy, e.g. 'coffee_shop'; name is a
substring match) — worth passing on a long route, since an unfiltered
corridor through a dense area can hold more places than the search
considers, in which case the response carries "truncated": true and a
note saying so.

Omit mode to use the stored preferences mode, else drive. Same cost model
and the same straight-line-distance caps as `route`, and the same
structured errors: route_too_long, no_graph_nearby, no_route,
unsupported_mode, and bad_request for non-finite/out-of-range
coordinates or an invalid max_detour_m.
neighborhood_verdictA

Life-decision neighborhood verdict, not a data dump.

Accepts a point plus free-form life context (household, mobility,
priorities) and returns a ranked verdict: strengths, weak points, and
the one thing to verify in person. Empty context still answers a
generic walk-first daily-needs check and says what was assumed.
Optional radius_m / minutes / mode override what the context implies
(no car / walk-first -> walk, bike -> cycle, car -> drive; default
walk, 15 minutes). Does not call out to extra remote APIs.

Returns a structured {"error": ...} for bad coordinates, an unknown
mode, a radius past the mode cap, upstream failure, or a degraded
schema. Missing street graph degrades to straight-line times with a
note rather than failing the verdict.
verify_claimsA

Grade spatial listing claims ("8 min to the metro", "shops on the doorstep", "green space nearby") against real routing and places data.

Free-text claim parsing needs an LLM — this tool takes already-decomposed
structured checks; the verify_listing_claims prompt teaches an agent how
to turn listing text into them. Each of claims (max 8; max 5 of kind
travel_time, since each costs a routed call) is one of:

- {"kind": "travel_time", "to_category": str|None, "to_name": str|None,
  "mode": "walk"|"cycle"|"drive" (default walk), "claimed_minutes": number}
  Finds the nearest place matching to_category (an Overture taxonomy
  slug) and/or to_name (a substring match), then routes to it and
  compares the routed minutes against claimed_minutes.
- {"kind": "count_nearby", "category": str|None, "name": str|None,
  "radius_m": number (default 500, capped at 2000), "claimed_at_least": int}
  Counts matching places within radius_m and compares against
  claimed_at_least.
- {"kind": "distance", "to_category": str|None, "to_name": str|None,
  "claimed_max_m": number}
  Straight-line distance (haversine, not routed) to the nearest match,
  compared against claimed_max_m.

Every kind needs at least one of its category/name fields; giving
neither is a bad_request. A category is an Overture taxonomy slug,
matched exactly (including its taxonomy descendants), never as a
substring — "park" does not match a parking garage; a name is a
substring match.

Verdict per claim: "confirmed" when the measured number is within the
claimed number x1.15 (count_nearby: measured count >= claimed),
"stretched" within x1.5 (count_nearby: count >= half the claim, floor
1), otherwise "false". A claim asserting a place exists at all, when
none is found within the search bound, is "false" with a note —
absence is a verdict, not an error. A claim the measurement cannot
decide is "unverifiable" instead of "false": a travel_time claim whose
place is found but cannot be routed to (no street graph nearby, or the
network doesn't connect the two points), or whose failing measurement
came from a size-cap-truncated street graph, and a count_nearby claim
whose claimed_at_least exceeds the row cap the count stopped at.

Returns {"results": [{"claim": <echo of the input>, "verdict":
"confirmed"|"stretched"|"false"|"unverifiable", "measured": {...
kind-appropriate minutes/count/distance_m, plus the matched place's id
and name when there is one}, "note": optional}, ...], "verdict_rule":
a one-line summary of the thresholds above}.

Returns a structured {"error": "bad_request", ...} for anything
malformed in claims (unknown/missing kind, more than 8 claims, more
than 5 travel_time claims, a missing or non-numeric claimed value,
neither target field given, or an unsupported mode), {"error":
"bad_request", ...} for invalid lat/lon, or a structured {"error": ...}
if the upstream dataset is unavailable or missing columns this tool
depends on.
optimize_routeA

Best order to visit several stops: multi-stop route ordering (a small TSP).

Answers "I have these five errands, what order costs least" — stops is a
list of 2-10 points, each a {"lat": ..., "lon": ..., "name": ...
(optional)} dict, a GERS id, or a free-text name, mixed freely. The
answer is the cheapest visiting order over the real street graph, not a
straight-line guess. Solved exactly (Held-Karp over the routed cost
matrix), so it is the optimum, not a nearest-neighbour approximation.
Any stop given by id/name that failed to resolve returns an indexed
error (stops[i]: ...) with candidates on ambiguity — the whole call
fails, not silently drops that stop.

Returns {"order": [stop indices, in visiting order], "legs":
[{"from_idx", "to_idx", "distance_m", "duration_s"}, ...],
"total_distance_m", "total_duration_s", "mode", "roundtrip", "export"}
— indices refer to the input `stops` list, and there is no
polyline/geometry — for a single pair's numbers on their own, call
`route`. export is the pocket handoff: a multi-stop Google/Apple Maps
directions URL (coordinates only — no Maps API), a GPX 1.1 document
with every stop as a waypoint, and a printable list that keeps any
names the caller passed. If a stop already carries confidence or
operating_status (from a prior place lookup), the response adds
verify_before_going naming the 1–2 weakest. Any stop given as an id or
name adds "resolved": [{"stop": i, "name", "id", "lat", "lon",
"matched_by"}, ...] for just those stops — plain {lat,lon} stops need
no echo and the key is absent when every stop was already coordinates.

keep_order=true visits the stops in the order you gave and never
reorders them: the itinerary is the caller's, and this tool supplies
routed (not straight-line) per-leg numbers, the totals, and the export
for it — one street-graph build for the whole run instead of chaining
`route` per leg. Use it whenever the order came from the user ("first
the bank, then the school, then home"); leave it off to be told the
cheapest order. The response echoes "keep_order": true, and "order" is
then just 0..n-1. start_index must stay 0 with it (there is nothing to
fix — the given order already starts where it starts), and roundtrip
still chooses whether the last leg closes back to the first stop, so a
one-way itinerary wants roundtrip=false.

start_index (default 0) is fixed as the first stop. roundtrip=true (the
default) returns to it; the closing leg is in "legs" but the start is not
repeated in "order". roundtrip=false is an open path that ends wherever
is cheapest. Omit mode to use the stored preferences mode, else drive. Same
cost model every routing tool uses; one-ways make the drive/cycle cost
matrix asymmetric and that is solved for exactly. The objective minimized
is total duration.

If some pair of stops has no route between them (disconnected road data),
the call still succeeds: that leg's numbers are a straight-line estimate,
the leg carries "estimated": true, and the response carries
"estimated": true plus a note naming the estimated legs — so a flagged
approximation, never a crash.

confirm=true after the user agreed to wait for a first-time street-graph
build (about 5–25 seconds). Pass it only after a needs_confirm reply
and they said yes. One gate for the whole call — every stop rides the
same graph, so it asks once, never once per leg. A warm or cached graph
never needs it. Omit confirm unless you just asked and they said yes.

Errors are structured, not raised: fewer than 2 or more than 10 stops, a
stop that is not a valid location reference, an out-of-range
start_index, or keep_order=true with a non-zero start_index return
{"error": "bad_request"} naming the offending stop
index; an unresolvable name/id returns {"error": "not_found"} or
{"error": "ambiguous_place", "candidates": [...]}, indexed the same way;
an unknown mode returns {"error": "unsupported_mode"}; a stop set whose
two furthest-apart stops are further apart than the mode's straight-line
cap (see `route`) returns {"error": "route_too_long"}; a stop with no
usable street node near it returns {"error": "no_graph_nearby"} naming
that stop's index.
preferencesA

Travel defaults.

State "I bike everywhere, I have a dog" once. Routing tools use the
stored mode when you omit theirs; an explicit argument always wins.
pace and household are stored for later features and do not change
answers yet. lang (#410) is the stored result-language preference: the
name-lookup tools that accept their own `lang` use this one when
theirs is omitted, returning an Overture-tagged name variant (e.g.
"Munich" for "München" with lang="en") — a per-call `lang` always
wins. The same document is the placeroot://preferences resource.

Call with no arguments to read. Pass mode, pace, household tags, a
free-text note, or lang to merge those fields.
clear=true deletes the file and cannot be combined with other fields.
Nothing is sent off this machine.
warmup_cityA

Pre-cache a city.

Copies places and transportation tiles into the same local cache later
queries read. Does not build the routing graph (the first route still
pays that cost) and does not pre-cache buildings. The warmup call is
the slow one; later place searches over the area read locally.

radius_m defaults to 8000 (a city core) and is capped at 25 km so a
warmup cannot fan into a planet-sized tile fetch.

confirm=true after the user agreed to wait for a first-time tile
warmup (about 5–25 seconds). Pass it only after a needs_confirm reply
and they said yes. An already-cached city never needs it.
Omit confirm unless you just asked and they said yes.
data_versionA

Which Overture Maps release backs the answers from every other tool.

Reports the active release string, its date, and whether it came from
live S3 discovery, an operator env override, or the pinned fallback
baked into this build. Resolved once at process start and cached for
the process lifetime — this tool just reports that cached value, it
doesn't re-check upstream, so it's small and has no upstream DB
dependency.

The body is resources.data_version_payload(), shared verbatim with the
placeroot://data-version MCP resource so the two surfaces cannot drift
(issue #195); tests/test_resources.py asserts they stay equal.

Prompts

Interactive templates invoked by user choice

NameDescription
site_selectionPick where to open a business: category lookup, area baseline, candidate comparison, competitor proximity, one recommendation.
compare_neighborhoodsCompare two neighborhoods side by side on amenity mix, density, and building stock.
plan_errandsOrder a list of errand stops into an efficient route with per-leg distances and durations.
should_i_live_hereShould I live in this neighborhood? One verdict shaped around the asker's life, not a table of counts.
get_to_know_my_cityPre-cache places and transportation tiles for a city — the optional first-five-minutes warmup. Does not build the routing graph or cache buildings.
verify_listing_claimsCheck a listing's spatial claims (travel time, nearby counts, distances) against real map data — confirmed / stretched / false / unverifiable, claimed vs. measured.
plan_area_visitPlan a visit around specific interests: one multi-category scan, an optimized route, and a map — trust tiers surfaced, not hidden.

Resources

Contextual data attached and managed by the client

NameDescription
data_versionWhich Overture Maps release backs every PlaceRoot answer, and how it was resolved (live S3 discovery, an operator env override, or the pinned fallback). Same value the data_version tool returns.
categoriesSummary of the Overture places category taxonomy: every top-level category with how many slugs sit under it, plus how to look up the exact slug for a query. Compact by design — use the search_categories tool for individual slugs.
preferencesLocal travel and household preferences (mode, pace, household, result-language lang). The same document the preferences tool reads and updates. Nothing in this file leaves the machine.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/chuofringer/placeroot'

If you have feedback or need assistance with the MCP directory API, please join our Discord server