feat: result-language preference — return Overture name variants (#410) - #412
Merged
Conversation
Adds a `lang` field to the stored preferences() document (2-3 lowercase letters, validated like mode) plus a per-call `lang` override on geocode/geocode_detailed, resolve_place, and place_details — the four name-heavy answer tools this round, matching find_places' own documented scope line (its rows, and resolve_place's place-kind candidates sourced from it, keep primary names). 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. No variant, or no lang requested, keeps the answer byte-identical to before — never invented or transliterated, only what's actually in the data. Query-cost design: divisions get a new small local lang_names.parquet table, materialized alongside #214's alt-name table from the same names.common scan (one more one-time build pass, not a per-request cost). A lang lookup at request time is one indexed join keyed by the page of result ids, not a scan per row. place_details piggybacks the variant onto its existing single-row places query as one extra selected column, with the same try/except-and-fall-back-to-no-variant convention #373's alt-name tier already uses for the same optional names.common field. Fixture work: tests/fixtures/divisions.parquet already carried real names.common data from #214's exonym corpus, so no change was needed there beyond the new query-layer plumbing. tests/fixtures/places.parquet had no names.common column at all; scripts/build_fixture.py now writes one (empty on every existing row, so no prior test's counts move) plus one new, deliberately isolated fixture row carrying real variants for the place_details tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Owner
Author
Lead review (loop)Re-ran locally: 2402 passed (+34), ruff clean. Verified in review:
Acceptance criteria of #410 met. Ready for owner merge (loop does not merge). |
…l lang - resolve_place's division match labels (the merged ranking's primary sort key) and its city-word token pruning now read the primary name (name_primary when a variant was applied): grading 'München' against a lang-swapped 'Munich' demoted the exactly-matched division below coincidentally-named places and stopped the city-hint pruning from firing. - preferences.resolve_lang now strips/lowercases an explicit per-call lang exactly like the stored write path, and a value failing the same shape check disables lang for that call instead of silently passing junk that can never match a variant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YCuTkstNj5KscUQ9u8cLMK
# Conflicts: # CHANGELOG.md # docs/benchmarks-vs.md # docs/benchmarks.md # src/placeroot/server.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #410. Adds a result-language preference: geocode/geocode_detailed, resolve_place, and place_details can now return Overture's language-tagged
names.commonvariant instead of a row's primary name.langfield on the storedpreferences()document (2-3 lowercase letters, validated by shape the waymodeis validated by membership).langoverride ongeocode,resolve_place, andplace_details— the four name-heavy answer tools named in the issue. Per-call wins over the stored preference.namebecomes the variant,name_primaryis added only when it differs from the primary. No variant, or nolangat all → byte-identical to the pre-Result-language preference: return Overture name variants in the user's language #410 answer, no note spam.names.commonmap.North-star demand: pelias/pelias#979 (structured search for cities in different language variants), pelias/pelias#967 (stale romanization). Competitive: Nominatim's
accept-language.Investigation findings (asked for in the task)
geocode.py's Search Overture's alternate names: geocode should match names.common, not just names.primary #214/Typo-tolerant + multilingual POI name search: extend the #214/#215 tiers from divisions to places #373 alt-name machinery already readsnames.common, but both existing tables discard the language key —_materialize_alt_names_tablegroups by(id, folded-spelling)across every language on purpose (search-only, dedup by spelling). There was no existing table that answered "does this id have a variant for this specific language."tests/fixtures/divisions.parquetalready carried realnames.commondata from the Search Overture's alternate names: geocode should match names.common, not just names.primary #214 exonym corpus (München→{en: Munich, ...}, Tokyo, Moscow, Vienna, Bratislava) — no fixture change needed there, only new query-layer plumbing.tests/fixtures/places.parquethad nonames.commoncolumn at all (confirmed viaDESCRIBE; also documented intest_poi_name_tiers.py's docstring). Both fixture-builder scripts (build_fixture.py,build_geocode_fixture.py) are fully local/synthetic (no live S3 scan) — extending them was the cheap, honest path, so I extendedbuild_fixture.pyto write acommonMAP column (empty on every existing row, so no prior test's counts move) plus one new, deliberately isolated fixture row ("Kaffeehaus Wien" at an unused coordinate) carrying real variants for theplace_detailstests.Query-cost impact
geocode/geocode_detailed, andresolve_place's division-kind candidates): a new small locallang_names.parquettable, materialized alongside the existing Search Overture's alternate names: geocode should match names.common, not just names.primary #214 alt-name table from the samenames.commonscan — one more one-time build pass per dataset materialization, not a per-request cost. At request time, applyinglangis one indexed join keyed by the page of result ids (_lang_variants_for), never a query per row, and it's skipped entirely whenlangisn't requested.place_details): piggybacks the variant onto the same single-row SQL query already readingnames— one extra selected column (names.common[$lang]), not a second scan. Falls back to no-variant (never fails the call) via the same try/except convention Typo-tolerant + multilingual POI name search: extend the #214/#215 tiers from divisions to places #373's alt-name tier already uses for this optional, unprobeable nested field.Deviations from the issue's literal scope
find_placesrows keep primary names (the issue itself draws this line: "find_places rows keep primary names this round to bound scope"). Sinceresolve_place's place-kind candidates are sourced fromfind_placesinternally, they inherit that same scope line and are unaffected bylang— onlyresolve_place's division-kind candidates (via its internalgeocode()call) get the swap. Documented in both the code and the CHANGELOG.geocode/geocode_detailed's own places-fallback rows (type: "place", found via_query_places_fallbackwhen no division matches) are also left unenriched this round, for the same reason and to keep the change bounded — a places-sidenames.commonlookup there would need its own query-cost design, which felt like scope creep beyond the issue's "M" effort sizing. Covered bytest_places_fallback_rows_are_unaffected_by_lang.Test plan
uv run pytest -q— 2402 passed (2368 baseline + 34 new,tests/test_lang_preference.py), 15 deselected (@live)uv run ruff check .— cleanname_primarypresence rule (present only when it differs)name_primary, no note)langconfigured at all → byte-identical answers (explicit guardrail tests)resolve_place's internal resolve-cache is keyed onlangtoo, so alang="en"call can't leak into a later unlangauged call@livetests addeddocs/REFERENCE.md(preferences tool/resource rows + profile section),CHANGELOG.md### Added,resources.py's resource description, and the generated schema docs (docs/benchmarks.md,docs/benchmarks-vs.mdviatoken_efficiency.py --write/competitor_comparison.py --write) all regenerated/updatedHouse-style checklist
name/name_primaryongeocode,resolve_place,place_detailsrows — additive only (new optional field, existingnamefield unchanged in shape, byte-identical whenlangis omitted)🤖 Generated with Claude Code