Read, match and display Chinese, Japanese and Korean text in a city's data and on its map - console and file encodings on Windows, bilingual OSM names, choosing a language edition, normalising CJK text before any match or de-duplication, the privacy check's blindness to CJK names, and the font order that decides which glyph forms a reader sees. Use before and during any city whose data carries Han, kana or Hangul - Hong Kong, the Taiwanese cities, Seoul and the Japanese cities - alongside add-city and address-join. Not for Latin-script cities.
Install
npx skillscat add dacekroberts/expanded-heatmap/cjk-text Install via the SkillsCat registry.
Chinese, Japanese and Korean text - processing it and showing it
Distilled from Hong Kong (2026-09-24), the first city whose data carried
Chinese characters, and written before the cities queued behind it: Taipei
(Regional), Taichung and Taoyuan (Traditional Chinese, registers in Chinese
only), Seoul (Hangul) and the Japanese cities (kanji and kana). Each of
those will meet what Hong Kong met, usually harder, because Hong Kong's registers
come in an English edition and theirs do not.
Every item is labelled. MEASURED means Hong Kong hit it and the fix is in
its code. TO CHECK means a known trap in this family that Hong Kong did not
reach; measure it in the city that does, then move it up a label here.
1. Encodings on this machine - MEASURED
This project runs on Windows, where Python's default text encoding is the ANSI
code page (cp1252), not UTF-8. Three things followed from that in one build:
- Printing a CJK name crashed a probe.
print()of MTR's station list raisedUnicodeEncodeError: 'charmap' codec can't encode characters. A crash
mid-probe looks like a data problem, and it is not one. Putsys.stdout.reconfigure(encoding="utf-8")at the top of any script that
prints names, or run it withPYTHONIOENCODING=utf-8. - A console dump is not evidence about the bytes. An ALS response piped
throughpython -m json.toolshowed its Chinese fields as mojibake
(ä¹\udc9d...) because the pipe was decoded as cp1252. The data was
fine. Judge an encoding from the file, opened with an explicit encoding, never
from terminal output. - Several BOMs can hide in the first header (Taiwan, MEASURED). Taichung's
portal serves CSVs that begin with U+FEFF FIVE times;utf-8-sigstrips one
and the first column still misses its lookup. Decode as UTF-8 andlstrip(chr(0xFEFF)). - A BOM hides in the first header. MTR's CSV begins with U+FEFF, so its first
column readLine Codeand a lookup by"Line Code"failed. Read CSVs
withencoding="utf-8-sig"; it strips a BOM when there is one and is harmless
when there is not.
The rule underneath all three: every open, read_text, write_text,read_csv and to_csv names its encoding. The pipeline already does; probes
are where it slips.
And one project rule bites harder here: a regex holding a Unicode escape
([㐀-鿿]) contains backslashes, so it cannot go in a Bash heredoc or-c string (.claude/hooks/block_heredoc.py). Write the probe to a file.
2. Names from OpenStreetMap - MEASURED
- Hong Kong's convention is a bilingual
name(荃灣 Tsuen Wan) plusname:en. One MTR stop node had noname:en. The rule inpipeline/hong_kong/step1_stations.py: prefername:en; failing that, take
the Latin tail ofnameonly when it matches CJK-then-Latin; failing that,
STOP and name the node, never guess. Count the fallback, and print it
(Hong Kong: 1 of 371). - Which tag carries the reader's name differs per country - TO CHECK before
step 1, on the city's own relations: Taiwanname:en/name:zh, Japanname:en/name:ja-Latn/name:ja_rm, Koreaname:en/name:ko-Latn.
Print the coverage of each before choosing. - Network tags are CJK strings (
港鐵 MTR,輕鐵 Light Rail). Match them
exactly, keep them in the city's config (a UTF-8 source file), and print the
relation table before matching - the osm-rail rule, unchanged. - When the operator's list and OSM disagree on a name, match on the ID.
Light Rail stop 250 is "Hoi Wong Road" to MTR and "Tuen Mun Swimming Pool" in
OSM; both carry stop ID 250, and that is what made the rename provable rather
than assumed. Record the alias with the ID that proves it.
3. Which language edition - MEASURED, and a decision
- Bilingual publishers ship paired editions and paired fields. FEHD
publishes_ENand_TCXML files; its CSDI layers pairNAME_EN/NAME_TC,ADDRESS_EN/ADDRESS_TC, and the shop sign asNSEARCH03_EN/_TC. Choose
the edition deliberately, say which on the page, and record why. - An English edition is still mixed-script. Hong Kong used the English
edition, and 7,159 of its in-ring tooltips contain Chinese anyway - shop signs
such as章記香港仔魚旦王orHarvest House 加賀屋 (Peace & Joyful). Plan for
CJK on the map in every CJK city, whichever edition is read. - Taiwan's, Japan's and Korea's registers are single-language - the brief
says so for Taiwan's national tax register. Their maps show names in
Chinese, Japanese or Korean only. That is correct, and every built page tells
the reader in one bullet under Reading the map (or The businesses),
in its family's approved wording (docs/city_page_format.md, section 1):- Japan: "Names and permit types are shown in Japanese, as the city records
them." - Taiwan: "Names and lines of business are shown in Chinese, as the register
records them." - Korea (Seoul, the model page): "Each dot carries the name on the permit, in
Korean, and its kind in English."
- Japan: "Names and permit types are shown in Japanese, as the city records
- A kind shown in English is reader-facing text, so it is American
English (owner, 2026-10-01; spec, section 4). Korea's English labels
(pipeline/taxonomies/korea_sbiz.py,korea_localdata.py) read "Watches
and jewelry", "Tires", "Liquor store", "Gas station", "Shopping center". The
register's own category name in its own script stays as written, as do
official names and licence titles. - Join on an identifier, never on text. Hong Kong's register and its point
layer joined by licence number; the shop signs were identical on 99.7% of
rows, which is high and still not a key.
4. Normalising before any match or de-duplication - partly MEASURED
upper()does nothing to Han, kana or Hangul. Hong Kong's de-duplication
key wasupper().strip()of shop sign and address, which is enough for the
English edition (2 duplicates found). For CJK text, normalise first withunicodedata.normalize("NFKC", s). It folds full-width Latin and digits
(ABC123toABC123), full-width parentheses (()) and the
ideographic space (U+3000). MEASURED for Taiwan (2026-09-25): the door-plate
numbers are full-width (175之2號) and the register pads one-character
district names with an ideographic space (臺中市中 區), so NFKC plus removing
U+3000 is required before any key -pipeline/countries/taiwan.nfkcdoes both.
TO CHECK: how much of Japan's text is
full-width - count before and after normalising.- Addresses insert units a join does not expect. Taipei's brief measured
its register inserting里and鄰between district and street, so the first
join matched nothing; it matches on district + street + number. Japanese
addresses carry丁目,番地and号, often with kanji numerals (一丁目)- TO CHECK against the city's address file. See
address-join.
- TO CHECK against the city's address file. See
- A CJK regex range must reach the supplementary planes. Hong Kong's step 1
uses[㐀-鿿](CJK Extension A and the unified block), which is
enough to spot a bilingual name. It is NOT enough to test "is this
character Chinese": Hong Kong's HKSCS characters and many Cantonese ones sit
above U+FFFF (𨋢, lift, is U+282E2). Use[㐀-鿿豈-\U00020000-\U0003134f]for Han, add-ヿfor kana and가-ᄀ-ᇿ-for
Hangul. Python'srehas no\p{Han}. - Publishers write placeholders, not blanks. 308 FEHD licences carry
No Recordor(no record found)in the shop-sign field; they show as
"No shop sign on the license". TO CHECK in each CJK register: the Chinese-,
Japanese- or Korean-language placeholder, which a Latin pattern will not see.
5. The privacy check cannot read these scripts - MEASURED
scripts/check_personal_exposure.py is built on Latin-script heuristics, and in
Hong Kong it misread in both directions:
- Its person-name heuristic flagged 14.7% of pins, and every one was a trade
name. It reads two capitalised words as a name: a shop sign
of two romanized Chinese syllables ("SURNAME GIVEN-NAME" in shape), a
romanized Japanese restaurant brand,Wealthy Garden. - Its residence test counts
UNITas residential. In Hong Kong, UNIT is how
commercial space is numbered. All 144 "person-like name at a residential unit"
hits were mall and office units (SHOP UNIT LG19, LG/F, THE SOUTHSIDE). - It cannot see a CJK personal name at all. A Chinese name is two to four
characters and a Korean one usually three syllables, with no capitals, no
comma and no space. So on a Chinese-only, Korean or Japanese register its
zero is not a finding.
Now built, two ways (2026-09-25): Seoul's korean=True pass
(pipeline/korean_names.py: a bare surname-plus-two-syllables name at an
address that reads residential is withheld; 138 withheld) and Taiwan'staiwan=True pass, which tests the owner's RULE rather than a name list (a sole
proprietor's name is shown only with a business marker; the marker list leaves
out characters that occur in given names - 美, 軒, and the surname 莊). Both print
a count that must be ZERO. Japan's japan=True pass followed (2026-09-27): it
tests the owner's name rule (japan-city) on what reached the map, and also
prints a count that must be zero.
For a CJK register no pass covers yet (a new pass belongs in the check rather
than here), do what Hong Kong did. Find out whether
the register carries a person's name at all - FEHD's carries the shop sign and
no licensee, which settled Hong Kong structurally. Then read a sample of the
flagged pins in the original script, decide, and record the verdict inDECISIONS.md.
6. Display on the map - MEASURED
- No missing glyphs.
FONT_STACKinpipeline/theme.pycarries Japanese,
Korean, Traditional and Simplified Chinese faces after the Latin ones, and
Hong Kong's tooltips rendered Chinese on a real hover with no empty boxes.
Keep the Latin faces first, as that file's comment explains: browsers fall
through per glyph, and a CJK face first would restyle every Latin name. - Pass the map's language:
render_heatmap(..., lang="zh-TW"), orzh-HK,zh-CN,ja,ko. Han unification gives Chinese and Japanese the
same code points with different regional glyph standards (骨and直are
the classic pairs), so whichever CJK face comes first draws every Han
character. Until 2026-09-24 the order was Japanese-first for every map, and
Hong Kong's signs rendered in Japanese forms. Measured with the browser's
own record of which font drew each glyph: Yu Gothic drew 3 of骨直嘅冇啲
and JhengHei the other 2, so one sign mixed two typefaces.langsets<html lang>and orders the CJK faces for that script
(pipeline/theme.font_stack); withzh-HK, JhengHei drew all five.render_heatmapnow RAISES when business names contain CJK characters
and nolangis given, so this cannot be forgotten. A new language needs its
faces and order added to_CJK_FACESand_CJK_ORDERintheme.pyfirst. - Languages used so far:
zh-HK(Hong Kong),ko(Seoul and the other
Korean cities),zh-TW(the Taiwanese cities),ja(the Japanese cities).
The render raised without one on every CJK city, as designed. - Tooltip metrics. A CJK face changes line height and width; the Hong Kong
deploy check saw no overflow, but a Chinese-only or Japanese-only register
puts CJK in every tooltip, so look again. - City names on the macro map are English ("Hong Kong", "Taipei
(Regional)"). Measure each label's width in the live app's frame with five
known widths reproduced, asscripts/check_macro_labels.pyrequires. - Page text. Space Grotesk has no CJK. If a page quotes a name in its own
script (the Japanese pages quote 国土数値情報), check it in the rendered page.
The page's layout and wording rules aredocs/city_page_format.md.
Checklist for a CJK city
- Probes print UTF-8, CSVs are read with
utf-8-sig, and encodings are never
judged from console output - OSM name-tag coverage printed per tag before choosing; any fallback rule
counted; unmatched nodes stop the step - Operator/OSM name disagreements resolved by an ID, recorded with it
- Language edition chosen and stated on the page, in its family's bullet;
English kind labels in American spelling; joins made on identifiers - NFKC before every text key; full-width share counted; placeholders found
in the register's own language - Privacy: does the register carry a person's name at all? A sample read in
the original script, with the verdict recorded - the heuristic's zero is
not one -
lang=passed to render_heatmap (it raises without one on CJK names);
the drawn font checked on a hovered tooltip, not assumed - Tooltips with CJK hovered in the rendered map, in both themes