Skip to content

Should a zero-width space separate words, so ​王小明 reads as 王小明 does? #851

Closed

Description

@derek73

A zero-width space (U+200B) carries no name content, but the tokenizer keeps it inside the word it touches. So an invisible character changes the reading.

>>> parse("王小明")            # family='王小明'
>>> parse("​王小明")      # given='​王小明'
>>> parse("​Dr. John Smith")
# given='​Dr.', middle='John', family='Smith'   (the title is missed)
>>> parse("John​Smith")
# given='John​Smith'                            (one word)

Under parser_for(locales.ZH), ​王小明 stays whole in given too: the name is no longer wholly Han, so neither the family-first script order nor segmentation applies.

Where it was found

The tw-donors local corpus (#844, fetched 2026-10-22, data dated 2026-10-05): 520 of its 308,667 distinct names open with U+200B, and each reads given where the same name without it reads family. 409 of those 520 also appear in the corpus without the character, so the same donors read two ways. None of the other local corpora (ctgov-officials, fec-individuals, ukec-individuals, psc-individuals, tse-donors) and none of the committed corpora holds a U+200B.

What the tokenizer does today

rules/T1 (separators, not joiners) makes "an ignorable character, one that carries no name content", a separator. The set it implements (_BIDI and _EMOJI_RANGES in nameparser/_pipeline/_tokenize.py) is the bidi controls (U+061C, U+200E, U+200F, U+202A to U+202E, U+2066 to U+2069) and emoji. U+200B is not in it, and Python's str.isspace() is False for it, so it reaches the token as a letter would.

Proposal, in rule shape

When a name holds a zero-width space (U+200B),

Then the name splits there, as at whitespace: the character is in no token, field or rendered view, and the original string keeps it.

Because U+200B carries no name content, and Unicode defines it as a word boundary with no width. Thai, Khmer and Burmese text write it between words where there is no space. That is T1's case, so it belongs in T1's set.

"​王小明"         →  family="王小明"
"​Dr. John Smith"  →  title="Dr."  given="John"  family="Smith"
"John​Smith"       →  given="John"  family="Smith"
"王​小明"          →  given="小明"  family="王"   (two wholly-Han words, read family-first, as "王‏小明" does today)

Excluded (stays inside the word, as today):

  • U+200C ZERO WIDTH NON-JOINER and U+200D ZERO WIDTH JOINER carry content: they decide how the letters around them join. The committed corpora hold them inside Malayalam, Kannada, Sinhala and Arabic-script (Persian) words (corpus_cldr.jsonl and corpus_wikidata_native.jsonl, 2026-10-22). Splitting there would cut real names in two.
  • U+2060 WORD JOINER means "no break here". Splitting at it reverses its meaning.

Open questions

  1. Under which switch? Unconditional, like whitespace and the nakaguro (rules/T2's precedent), or behind a Policy opt-out like strip_bidi? strip_bidi itself is the wrong switch, since U+200B is not a direction control.
  2. U+FEFF, the byte-order mark, also called ZERO WIDTH NO-BREAK SPACE: as a no-break space it is a joiner, but in names it nearly always arrives as a stray BOM at the start of a CSV field. No corpus held one on 2026-10-22. Strip it at a word's edge only, or leave it out?
  3. Separator or transparent at the interpunct? T3's flank guard reads through the stripped bidi marks (_flank) but not through whitespace. Should 威廉·​莎士比亚 behave like the bidi case or like a space? Today 威廉·‏莎士比亚 reads given 威廉, family 莎士比亚, as 威廉·莎士比亚 does, while 威廉· 莎士比亚 reads given 威廉·, family 莎士比亚.
  4. U+00AD SOFT HYPHEN is out of scope. It sits inside words, so separating there would split them, and T1 never joins. 20 names in the tse-donors local corpus held one inside a word on 2026-10-22, read today as one word with the hyphen kept, which may already be right.

What it would move

No committed corpus holds a U+200B, so tests/v2/test_snapshot.py and the CLDR/GND agreement records would not move. The local tw-donors snapshot would move its 520 names, from given to family.

Activity

  1. self-assigned this
    on Oct 23, 2026
  2. derek73 commented on Oct 25, 2026

    @derek73
    OwnerAuthor

    Rescan, 2026-10-24, on master at 26f7bc1. Every committed corpus and all eight local corpora, including jrc-names (#865) and nepal-candidates (#866), which landed after this issue was written. The scan counted every format character (category Cf) plus U+00AD and U+FEFF.

    The four examples above still read as described.

    U+200B: no change

    It is still only in tw-donors: 520 names, every one with U+200B at the start (one has two). All 520 read given where the same name without it reads family. The new corpora hold none, so "What it would move" stands.

    U+FEFF: now seen (open question 2)

    The issue says no corpus held one on 2026-10-22. jrc-names holds 12, and none is a byte-order mark at the start of a field. Each sits on the space between two words, either after it or before it:

    'Jerome Kerviel'    given='Jerome'    family='Kerviel'
    'Mathieu Kerekou'   given='Mathieu'  family='Kerekou'
    'Андерс Фог Расмуссен',  'حيدر علي',  'محمد تقي', ...
    

    None of the 12 reads differently from its twin without the character, because none touches a word that a vocabulary or shape test reads. The character still ends up inside the field, though, and it hides a word from those tests just as U+200B does: parse("Dr. John Smith") gives given Dr., middle John, family Smith. All 12 sit at a word's edge, so either answer to question 2 (strip at a word's edge, or separate as U+200B would) covers every case seen so far.

    U+00AD: half of it is mis-decoded text (open question 4)

    All 20 names in tse-donors that hold U+00AD have it right after Ã: NATALÃ\xadCIO is NATALÍCIO decoded with the wrong encoding, since Í and í in UTF-8 both end in the byte 0xAD. In jrc-names, 30 of 46 are the same kind (AlA\xad for Alí). The other 16 are real soft hyphens left in from typesetting (Carles Puig\xaddemont, Harvey Wein\xadstein, Ayman al-Zawahiri\xad). That doesn't change the scope call: separating there would still split real words. But removing a soft hyphen inside a word, rather than separating at it, would be a separate question from this issue.

    Related, not in this issue's scope: U+200C and U+200D at a word's edge

    The exclusion of the zero-width non-joiner and joiner holds where they sit between letters: 8,740 joiners in nepal-candidates alone (श्रेष्‍ठ). But 575 sit at a word's edge, where there is nothing to join: 425 in jrc-names (Antonio ‌Negri, نواز‍ شریف) and 150 in nepal-candidates (लामिछाने‍, and वि‌.क., inside a paired-initials spelling). Removing them only at a word's edge would leave the content-carrying ones alone. I haven't measured whether any of the 575 change a reading.

  3. added this to the 2.6 milestone on Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions