Skip to main content

Immich Traditional Chinese Geodata (4): Translating Place Names with Wikidata, and How It Fails Silently

··1590 words·4 mins
Table of Contents
immich-geodata-zh-tw - This article is part of a series.
Part 5: This Article

The previous post, Five Regions, Five Strategies, noted that place names written in non-Han scripts, as in Thailand and Indonesia, can only go down the translation route, and that the translation source this project settled on is Wikidata.

What follows is not about how to query. SPARQL itself is not the hard part. The hard part is this: when Wikidata gets it wrong, there is usually no sign at all. The pipeline does not stop, the log reports no error, and the output is a perfectly valid Chinese string that simply points to the wrong place.

Why Wikidata
#

Place-name data has a few properties that make general-purpose translation a poor fit:

  1. Duplicate names are everywhere: South Korea has a Jung-gu in Seoul, another in Busan, another in Daegu. Thailand and Indonesia are equally full of repeated district names. A translation API sees no context and cannot tell them apart.
  2. Place names are a lookup, not a semantic translation: what “Ngawi” should become depends on the Chinese rendering already conventional in usage, not on the literal meaning of the word.
  3. A verification channel is required: the result has to be confirmable as “this really is that administrative division”, not merely “this string reads plausibly”.

Wikidata fills exactly that gap. It is a structured knowledge graph in which every geographic entity carries:

  • P131 (located in the administrative territorial entity): an explicit statement of the parent administrative division.
  • P31 (instance of): distinguishes a city from a district, a train station, or a government body.
  • Multilingual labels: including Chinese variants such as zh-tw and zh-hant.

With P131 available, duplicate names can be settled by parent division. When querying Jung-gu (중구), additionally require its P131 chain to contain Seoul (Q86), and Busan’s Jung-gu falls out on its own.

SELECT ?item ?itemLabel WHERE {
  # Bind the Korean name directly; do not FILTER over every label in the graph (that will almost certainly time out)
  ?item rdfs:label "중구"@ko.

  # The key step: verify it really sits on Seoul's administrative chain
  ?item wdt:P131+ wd:Q86.

  SERVICE wikibase:label { bd:serviceParam wikibase:language "zh-tw,zh-hant,zh,en". }
}

Up to this point, everything looks fine.

Failure Is Silent
#

Run this in practice and you find that the pipeline fails in two ways, and that the two look nothing like each other:

The first: no entity is found, or every candidate fails the containment check. The result falls back to the source-language original, and that place name stays untranslated. This is actually the good outcome. It lands in the untranslated list, visible at a glance.

The second: the wrong entity gets selected, and that entity passes the P131 check. The translated name is taken straight from the wrong entity. This is the hardest one to notice, because the output is a valid Chinese string that never appears in any anomaly list.

A real instance of the second kind: Seoul’s 관악구 (Gwanak-gu, 冠嶽區) was once emitted as 新林洞 (Sillim-dong). Gwanak-gu really does contain Sillim-dong, and Sillim-dong’s P131 chain really does contain Seoul, so the check passes cleanly. Nothing in the data looks wrong, unless somebody sits down and compares every single district name.

Three outcomes of Wikidata place-name translation: passing verification yields the correct name; finding nothing falls back to the original and shows up in the untranslated list; selecting the wrong entity yet passing verification yields an invisible mistranslation
Of the three paths, only the fallback to the original is a visible failure

Six Known Ways to Get It Wrong
#

These are the forms the project has actually run into and recorded:

Failure modeCaseHow to spot it
Search selects the wrong entity관악구 matched 신림동, a neighborhood inside it; 송파구 matched the subway station 잠실역The candidate’s source-language label is not equal to the queried name
Upstream label stuck on an older designationThe Chinese label for 여주시 used to be 驪州郡, the name it held before promotionThe administrative-level suffix does not line up (시 → 市, 군 → 郡, 구 → 區)
Claims to be Traditional, but the glyphs are wrongThe zh-tw label for 함평군 turns 咸 into 鹹; Indonesia’s Papua leaves the Simplified form 巴布亚 sitting in zh-hantManual spot checks, or a scan against a list of Simplified-only characters
P131 statement missing영종구 had none at all for a while after the district was createdThe containment check fails and no statement can be found
P131 marked deprecatedThe statement placing Indonesia’s Kabupaten Ngawi in East Java was once marked deprecatedSPARQL’s wdt: prefix follows only best-rank statements, so deprecated is equivalent to absent
Our own filter rules kill the correct candidateSee the next sectionThe correct entity is simply not in the candidate set

The fifth one deserves particular attention, because it runs entirely against intuition. The Kabupaten Ngawi data was still sitting on Wikidata the whole time, just flagged as deprecated, and the wdt: prefix only ever follows best-rank statements. As far as the query is concerned, that statement does not exist.

The Most Ironic One: The Safeguard Manufactures the Error
#

The sixth mode is entirely our own doing.

To stop the search from landing on government bodies (district offices, education bureaus, that sort of thing), the original approach was a keyword blacklist: walk through every label retrieved for a candidate entity and discard anything containing characters like 廳 or 所.

And then the correct 관악구 got discarded. Its zh-hant label had been imported by a bot from a Chinese Wikipedia infobox as 冠嶽區, which is the name of the district office, not the name of the district. The blacklist duly recognized 廳 and kicked out the one correct candidate. The closest thing left in the pool was that Sillim-dong.

A blacklist of exclusion rules can never be completed, and it will take out a correct candidate over a label in some entirely unrelated language. The South Korea handler was later switched to a whitelist test: the candidate’s source-language label must be an exact match for the queried name. Government bodies, job titles, electoral districts and stations never carry exactly the same name as the administrative division, so they drop out on their own, and no keyword list needs maintaining.

Warning

The Indonesian processing path still retains a similar keyword-matching mechanism, covering both Chinese-family labels and wiki article titles, which is precisely the scope that killed 관악구 in the first place. No false positives have been observed for Indonesia so far, but the risk is known.

Verification Methodology: Counts Are Not Enough
#

These failure modes lead to one operating principle: when verifying data, compare the full list of untranslated names, never just the count.

A real case: when Indonesia moved from the old dataset to the new one, the number of untranslated second-level divisions held steady at 51, apparently unchanged. Only a line-by-line comparison revealed that Kabupaten Ngawi had dropped out, while a different place name had been translated successfully for the first time, the two canceling out exactly.

By the same token, a rise in the untranslated count cannot be taken at face value as “upstream added new divisions”. New divisions upstream and a translation that has stopped working look identical in the numbers.

Caching: Layered, and It Short-Circuits
#

SPARQL queries are slow, so caching is not optional. The cache file lives at geoname_data/{country_code}_wikidata_cache.json in the project (KR_wikidata_cache.json, for example) and is split internally into several layers:

  • cache.search: search results
  • cache.labels: an entity’s multilingual labels
  • cache.instance_of: P31 classes
  • cache.p131: containment verification results
  • translations: the final naming decision

There is a pitfall here that shows up in practice. A translations hit has to satisfy two conditions at once: the parent QID computed on this run matches the one recorded in the cache, and that cached entry is either a translation failure or has already passed containment verification. When both hold, the pipeline takes the cached result directly and never reads cache.p131 at all.

Which means that after fixing a P131 statement upstream, clearing only cache.p131 and re-running will appear to change nothing, because that layer is never reached. To re-verify a single entity, both cache.p131 and translations have to be cleared. If what changed is the candidate filtering logic, re-running against an empty cache is less trouble.

Fix Upstream First
#

Last comes a principle about direction: if a translation error originates in Wikidata itself, go and fix Wikidata rather than adding another mapping entry inside the project.

Fixing upstream benefits every user. A mapping table benefits only this project, and it has to be maintained indefinitely. The South Korea example came up in the previous post: the seven manual mapping entries originally planned to patch upstream errors dropped to zero once Korean Wikipedia’s hanja notation was adopted as the name source.

The core lesson of the translation route is roughly this: do not translate what you can avoid translating, make what you must translate verifiable, and fall back honestly to the original when verification fails. A place name that visibly went untranslated is worth far more than a wrong one that looks entirely reasonable.

References
#


The next post, Rebuilding Taiwan’s Administrative Divisions from Official Datasets, returns to Taiwan and looks at just how simple the pipeline can get when an official dataset is available.

immich-geodata-zh-tw - This article is part of a series.
Part 5: This Article

Related

Immich Traditional Chinese Geodata (3): Five Regions, Five Strategies

··1823 words·4 mins
The first two posts covered the mechanism and the pipeline: Immich reads place names from cities500.txt, and that file is produced from national mapping data by extract and release. Technically, “how to swap the data” is a settled question. The genuinely hard part is something else: what should a foreign place name look like so that it reads naturally to a Taiwanese user? immich-geodata-zh-tw currently handles five regions, and it gives five different answers. This post is about the criterion behind those answers.

Immich Traditional Chinese Geodata, Part 2: The Data Pipeline

··2583 words·6 mins
The previous post in this series, How Reverse Geocoding Works, took apart the way Immich reads geographic data: a handful of plain text files get imported into PostgreSQL at startup, and a nearest-neighbour query resolves a place name when a photo is uploaded. Since swapping the files is enough to swap what gets displayed, the remaining question is about those “better files” themselves. This post takes apart the immich-geodata-zh-tw pipeline, from each country’s official map data all the way to the release.tar.gz that users download.

Immich Traditional Chinese Geodata Deep Dive (1): How Reverse Geocoding Actually Works

··1809 words·4 mins
Every time you upload a photo to Immich, the system automatically tags where it was taken, say “Xinyi District, Taipei” or “Shibuya, Tokyo”. That is not the work of a cloud API. It is a reverse geocoding system that runs entirely offline. Because it runs offline, there is room for a project like immich-geodata-zh-tw to exist (for the actual installation steps, see the illustrated setup guide in the first post of this series). Immich reads place names from a handful of plain text files, so replacing those files changes the place names it displays. This is the first technical post in the series, and it lays the groundwork: what actually happens when Immich resolves a place name, which files it reads, and what room that mechanism leaves for us to work with. Everything in the later posts, the per-country strategies, the translation work, the validation, builds on this.