Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ fixing behavior is patch.
## [Unreleased]

### Added
- Result-language preference (#410, north-star: pelias/pelias#979 and #967;
competitive: Nominatim's `accept-language`). A `lang` field (2-3 lowercase
letters) on the stored `preferences()` document, plus a per-call `lang`
override on `geocode`, `resolve_place`, and `place_details` (per-call
wins). When Overture's `names.common` carries a language-tagged variant
for the matched row, `name` becomes that variant and `name_primary` is
added only when it differs from the primary — never invented or
transliterated, and never a payload change when no variant exists or no
`lang` is given. Divisions get a new small local `lang_names.parquet`
table (materialized alongside #214's alt-name table, from the same
`names.common` scan) so a lang lookup is one indexed join by the page of
result ids, not a second scan; `place_details` piggybacks the variant
onto its existing single-row places query as one extra column. Scoped to
the four name-heavy answer tools this round — `find_places` rows (and,
through it, `resolve_place`'s place-kind candidates) keep primary names,
matching `find_places`' own documented scope line.
- `PLACEROOT_HOME=<city/area>` (#406, docs/ROADMAP.md next tier): a home
region, resolved once lazily through the same geocode ranking and cached
for the process, biases `geocode`/`geocode_batch`/`resolve_place` (and
Expand Down
8 changes: 5 additions & 3 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Every tool returns a compact, budgeted answer. Several single-item tools have a
| `geometry_op` | Geometry math and predicates behind one `op` catalog: point distance/bearing/destination/midpoint, area/length/bbox/centroid, buffer/convex hull (returns geometry), point-in-polygon, nearest point, nearest point on a line, union/intersect/difference (returns geometry) |
| `warmup_city` | Pre-cache a city's places and transportation tiles so later place searches over it read locally — does not build the street graph or cache buildings. A first tile COPY without `confirm` returns `needs_confirm` |
| `data_version` | Which Overture release the answers are drawn from |
| `preferences` | Read or update local travel/household defaults (mode, pace, dog, …). Nothing leaves the machine |
| `preferences` | Read or update local travel/household defaults (mode, pace, dog, result language, …). Nothing leaves the machine |


## Confirming a slow hop
Expand Down Expand Up @@ -124,7 +124,7 @@ context, so you can pin them into a conversation without spending a tool call:
| Resource | Contents |
|---|---|
| `placeroot://data-version` | The resolved Overture release, its date, how it was resolved (discovery, env override, the pinned fallback, or held at the artifact release), its age, and whether the bundled acceleration applies to it. Same values the `data_version` tool returns — one shared code path, so they cannot drift. |
| `placeroot://preferences` | Local travel and household preferences (mode, pace, household). Same document the `preferences` tool reads and updates — one shared code path. Nothing in this file leaves the machine. |
| `placeroot://preferences` | Local travel and household preferences (mode, pace, household, result-language `lang`). Same document the `preferences` tool reads and updates — one shared code path. Nothing in this file leaves the machine. |
| `placeroot://categories` | Summary of the place-category taxonomy: all 22 top-level categories with how many slugs sit under each, plus how to get an exact slug. ~530 tokens — a summary, not the 2,117-slug CSV, which stays behind `search_categories`. |

In Claude Code they auto-complete as @-mentions:
Expand Down Expand Up @@ -201,7 +201,9 @@ union of everything named:
`data_version` and `preferences` are registered under every profile.
`data_version` is ~230 tokens and the only way an agent can tell which
Overture release backs its answers. `preferences` is the local defaults
document; routing tools read its stored mode when theirs is omitted.
document; routing tools read its stored mode when theirs is omitted, and
`geocode`, `resolve_place`, and `place_details` read its stored `lang`
(#410) the same way.

Profiles may overlap, and a list may mix them with bare tool names —
`PLACEROOT_TOOLS=routing,find_places` or
Expand Down
8 changes: 4 additions & 4 deletions docs/benchmarks-vs.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ Regenerate with `uv run python benchmarks/competitor_comparison.py --write`. Eve
- Snapshots captured: **2026-08-08**
- PlaceRoot's own answers were captured on **macOS-26.3.1-arm64-arm-64bit** (Python 3.11.15, Overture `2026-08-19.0`) and are snapshotted rather than recomputed here: floating-point differences in routing and geometry change digit counts between platforms, so a live rerun costs a few tokens more or less on Linux than on macOS. A tolerance test reruns them for real and fails if this snapshot drifts from what the code now answers.
- Schema figures are counted twice: over the **common fields** every server here publishes, and **verbatim** over everything it sends. `outputSchema` is one of the common fields now that both sides declare one (roadmap §4.3) — the ratios below are the common-field ones; see the note under the table for the size gap that remains.
- Schema surface, whole install (common fields): PlaceRoot **32290** tokens (42 tools) · Mapbox **28958** (29 tools) · Google Maps **655** (7 tools)
- Schema surface, the six tools each server needs for the scenarios below (common fields): PlaceRoot **7529** · Mapbox **13069** (1.7x ours) · Google Maps **500** (5 tools — no isochrone tool exists)
- Whole-install surface: Mapbox is **0.9x** PlaceRoot's on common fields (both sides' output schemas included), on 29 tools against 42. Verbatim — counting the rest of what each side sends beyond the common fields — it is 0.9x (29295 against 32290), and 1.7x on the six-tool subset.
- Schema surface, whole install (common fields): PlaceRoot **32913** tokens (42 tools) · Mapbox **28958** (29 tools) · Google Maps **655** (7 tools)
- Schema surface, the six tools each server needs for the scenarios below (common fields): PlaceRoot **7708** · Mapbox **13069** (1.7x ours) · Google Maps **500** (5 tools — no isochrone tool exists)
- Whole-install surface: Mapbox is **0.9x** PlaceRoot's on common fields (both sides' output schemas included), on 29 tools against 42. Verbatim — counting the rest of what each side sends beyond the common fields — it is 0.9x (29295 against 32913), and 1.7x on the six-tool subset.
- Answers, over the 6 scenarios both PlaceRoot and Mapbox answer: PlaceRoot **1685** tokens total, Mapbox **1337** (1219 with pretty-print whitespace removed)

### Where the competitor numbers come from
Expand All @@ -157,7 +157,7 @@ Regenerate with `uv run python benchmarks/competitor_comparison.py --write`. Eve

| server | tools registered | whole install, verbatim | whole install, common fields | the 6-scenario subset | subset verbatim | subset common fields |
|---|---:|---:|---:|---:|---:|---:|
| PlaceRoot | 42 | 32290 | **32290** | 6 | 7529 | **7529** |
| PlaceRoot | 42 | 32913 | **32913** | 6 | 7708 | **7708** |
| Mapbox MCP | 29 | 29295 | **28958** | 6 | 13129 | **13069** |
| Google Maps MCP (archived) | 7 | 655 | **655** | 5 | 500 | **500** |

Expand Down
18 changes: 9 additions & 9 deletions docs/benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ Generated 2026-08-25 by `uv run python benchmarks/token_efficiency.py --write`.
- Token counting method: **chars/4 heuristic (no tokenizer installed; same estimator as placeroot.budget.estimate_tokens)**
- Overture release pinned for the fixture run: `2026-08-19.0`
- Tools registered: **42**
- Total schema surface: **32290 tokens** (129218 chars, 129562 bytes)
- Of which inputSchema: **5139 tokens**, outputSchema: **3891 tokens** (roadmap §4.3, #403) — the rest is names/descriptions/annotations
- Schema cost per tool: min 261, median 651, max 2971 tokens
- Total schema surface: **32913 tokens** (131718 chars, 132077 bytes)
- Of which inputSchema: **5419 tokens**, outputSchema: **3891 tokens** (roadmap §4.3, #403) — the rest is names/descriptions/annotations
- Schema cost per tool: min 261, median 680, max 2971 tokens
- Median scenario response: **436 tokens** (range 87-538)
- Break-even: the schema surface costs about as much as **74 median answers**
- Break-even: the schema surface costs about as much as **75 median answers**

### Schema surface (paid once per conversation)

Expand All @@ -92,12 +92,12 @@ Generated 2026-08-25 by `uv run python benchmarks/token_efficiency.py --write`.
| `changes_in_area` | 1161 | 212 | 52 | **1515** | 6060 |
| `optimize_route` | 799 | 132 | 324 | **1342** | 5371 |
| `meeting_point` | 1018 | 106 | 52 | **1265** | 5063 |
| `resolve_place` | 769 | 182 | 206 | **1239** | 4959 |
| `geocode` | 796 | 114 | 195 | **1180** | 4722 |
| `geometry_op` | 703 | 328 | 52 | **1162** | 4648 |
| `compare_areas` | 885 | 105 | 52 | **1122** | 4489 |
| `travel_time_matrix` | 707 | 139 | 173 | **1099** | 4399 |
| `resolve_place` | 688 | 112 | 206 | **1086** | 4344 |
| `suggest_areas` | 803 | 100 | 52 | **1039** | 4156 |
| `geocode` | 688 | 44 | 195 | **1001** | 4004 |
| `verify_claims` | 781 | 68 | 52 | **989** | 3957 |
| `from_to` | 450 | 213 | 227 | **957** | 3830 |
| `render_map` | 597 | 136 | 52 | **857** | 3430 |
Expand All @@ -107,20 +107,20 @@ Generated 2026-08-25 by `uv run python benchmarks/token_efficiency.py --write`.
| `infrastructure_at` | 494 | 118 | 52 | **738** | 2953 |
| `ground_location` | 463 | 150 | 52 | **734** | 2937 |
| `distance_matrix` | 401 | 93 | 136 | **698** | 2795 |
| `place_details` | 342 | 228 | 52 | **680** | 2723 |
| `find_near` | 242 | 126 | 222 | **651** | 2606 |
| `within_distance` | 361 | 156 | 52 | **637** | 2549 |
| `geocode_address` | 407 | 108 | 52 | **630** | 2522 |
| `address_at` | 427 | 54 | 52 | **597** | 2388 |
| `gers_lookup` | 385 | 74 | 52 | **575** | 2303 |
| `place_details` | 265 | 158 | 52 | **532** | 2130 |
| `preferences` | 206 | 216 | 52 | **532** | 2131 |
| `neighborhood_verdict` | 200 | 152 | 52 | **462** | 1849 |
| `geocode_batch` | 211 | 58 | 98 | **430** | 1722 |
| `land_use_at` | 274 | 41 | 52 | **429** | 1717 |
| `search_categories` | 266 | 46 | 52 | **426** | 1706 |
| `resolve_place_batch` | 248 | 42 | 52 | **406** | 1626 |
| `elevation_at` | 242 | 41 | 52 | **394** | 1579 |
| `warmup_city` | 175 | 109 | 52 | **391** | 1565 |
| `preferences` | 135 | 146 | 52 | **389** | 1557 |
| `summarize_area` | 148 | 105 | 52 | **365** | 1460 |
| `data_version` | 166 | 16 | 119 | **355** | 1421 |
| `buildings_at` | 151 | 91 | 52 | **349** | 1399 |
Expand All @@ -129,7 +129,7 @@ Generated 2026-08-25 by `uv run python benchmarks/token_efficiency.py --write`.
| `simplify_geometry` | 129 | 58 | 52 | **297** | 1188 |
| `admin_lookup` | 145 | 41 | 52 | **296** | 1185 |
| `reverse_geocode` | 111 | 42 | 52 | **261** | 1044 |
| **all 42 tools** | 20358 | 5139 | 3891 | **32290** | 129218 |
| **all 42 tools** | 20695 | 5419 | 3891 | **32913** | 131718 |

### Response cost (paid per tool call, measured on committed fixtures)

Expand Down
24 changes: 22 additions & 2 deletions scripts/build_fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,19 @@ def build_place_rows() -> list[tuple]:
def add(
name, lat, lon, category, basic_category, status, confidence, alternates=None,
addresses=None, websites=None, phones=None, socials=None, brand=None, sources=None,
common=None,
):
# #410: `common` mirrors Overture's names.common on a places row —
# written for every row (empty when there are no language-tagged
# variants) so the fixture's `names` struct has the same shape the
# real places theme does and place_details'/resolve_place's lang
# lookup runs against it unchanged, the same convention
# build_geocode_fixture.py's divisions `common` column already uses.
index = len(rows)
rows.append((
gers_id(index),
{"xmin": lon, "ymin": lat, "xmax": lon, "ymax": lat},
{"primary": name},
{"primary": name, "common": common or {}},
{"primary": category, "alternates": alternates or []},
basic_category,
status,
Expand Down Expand Up @@ -190,6 +197,19 @@ def add(
add("Dateline East", 10.0, -179.98, "restaurant", "restaurant", "open", 0.7)
add("Dateline East Bank", 9.999, -179.985, "bank", "bank", "open", 0.65)

# #410: a place carrying names.common language variants, so place_details'
# and resolve_place's lang lookup has a real one to swap in. Appended at
# the end (not spliced into the dense cluster, so no earlier row's GERS
# id — derived from its insertion index — shifts) AND at an isolated
# coordinate well clear of CENTER_LAT/CENTER_LON, HIGH_LAT_CENTER, the
# antimeridian cluster and (0.0, 0.0): several tests assert exact place
# counts within a radius of those points (e.g. radius_m=100 at CENTER),
# and a new row inside any of those windows would silently change them.
add(
"Kaffeehaus Wien", 5.0, 100.0, "coffee_shop", "coffee_shop", "open", 0.8,
common={"en": "Vienna Coffee House", "fr": "Maison du Cafe Viennois"},
)

return rows


Expand Down Expand Up @@ -414,7 +434,7 @@ def build_places(con: duckdb.DuckDBPyConnection) -> None:
CREATE TABLE places (
id VARCHAR,
bbox STRUCT(xmin DOUBLE, ymin DOUBLE, xmax DOUBLE, ymax DOUBLE),
names STRUCT("primary" VARCHAR),
names STRUCT("primary" VARCHAR, common MAP(VARCHAR, VARCHAR)),
taxonomy STRUCT("primary" VARCHAR, alternates VARCHAR[]),
basic_category VARCHAR,
operating_status VARCHAR,
Expand Down
Loading