Localization¶
Purpose¶
How every place shows fully localized (name + city + country + region) in any supported language, and is searchable by a localized query, at catalog scale (millions of locations). This is the system-wide contract for place localization; it applies to every location regardless of source.
Principle: store each localized geography name once, reference it, assemble at read¶
Copying Japan → 日本 onto every row in Japan does not scale: it grows as
locations × fields × languages. Localized names are instead bounded by
geography, not by locations.
- A location stores its own name localizations in
Location.Localizations(a BCP-47 keyed map). A place's name is unique, so this cannot be shared. - Shared geography is referenced, not copied. Cities are location rows, so
city_idis a self-reference to the city's own row, and the localized city name is that row's ownlocalizations. Countries and regions are small reference tables, each carrying alocalizationsmap, keyed bycountry_code/ region id.
Read-time assembly¶
A localized card for viewer locale L resolves:
name = own.localizations[L]; city = cities[city_id].localizations[L];
country = countries[country_code].localizations[L]; region = regions[region_id].localizations[L],
each falling back to the base value when a language is missing.
- Countries (a small fixed set) are cached in memory; localized country is an O(1) lookup with no database round-trip.
- Cities are resolved by a batched, indexed lookup per page or viewport (the
distinct
city_ids of a result set), never per row. - Fixing a localized name once in the gazetteer updates every location that references it; there is no per-row copy to drift.
Capturing localizations¶
Localizations are captured from each source's native multilingual data, at the point the data enters the catalog:
| Source | Provides |
|---|---|
Wikidata labels (wbgetentities) |
curated + reference names across the supported languages |
GeoNames alternateNamesV2 |
city names across the supported languages |
OpenStreetMap name:xx tags |
POI names per language |
Photon reverse-geocoding (its multilingual planet index) is an optional gap-filler for a place or language a source did not cover. It is never a hard dependency: the source data already carries the bulk of coverage.
The supported set is the languages the Photon planet index serves; the catalog is multilingual independent of which UI locales the app ships, and API payloads carry only the languages relevant to the viewer.
Localized search¶
The same localization store powers search, via two paths:
- Name search, any language. A per-location
search_textcombines the base name and all of its localized names, indexed by PGroonga (normalized withNormalizerNFKC150for width/kana folding). One index tokenizes Latin and CJK alike, so a query in any language or script matches, and a cost-gated fuzzy pass adds edit-distance typo tolerance. See Locations → Search & ranking. - Geo-scoped queries. A localized place name that denotes a city or country
("京都", "in Japan") resolves against the gazetteer's localized names, yielding
a geography id that filters locations by
city_id/country_code.
Invariants & gotchas¶
- Localized geography is never copied onto a location. Cities/countries/regions are referenced by id; only a location's own name is stored on the row.
- Cities are locations, so
city_idis a self-reference; there is no separate cities table. - Capture at ingest, not at read. Localizations land with the data; the read path assembles and caches, it does not fetch translations.
- Timezone is not a localization — it is derived from coordinates. See Timezones.
Cross-links¶
- Where seeded localizations come from: Catalog seeding.
- The place model: Locations.