Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/design/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,7 @@ Declined:
- 2026-08 #308 — an entry peels only where it can never end a name: 씨/님/さん/様/先生 peel; 양/군/氏/博士/殿 stay spaced-only; 君 is in neither set while its kana spelling くん peels. The vocabulary carries the license, so no structural or per-script gate stands over the peel.
- 2026-08 #312 — the peel crosses the family comma and the 间隔号: both answer where a name DIVIDES into surname and given, a question the peel never asks. Provenance matters here: neither gate was argued for the peel specifically — both came with the placement (#312's own framing) — so the crossing was a repair of inherited gates, not a designed-in property.
- 2026-08 #319 — a wholly suffix-shaped second run is declined as a peel site (the "田中さん, V." shape), but only when the name's own run offers a site, since a glued honorific is itself part of what makes a run read as suffix-shaped.
- 2026-10-17 #671 — SPACED AND AFTER-COMMA ONLY, no glued peel (Derek, 2026-10-17). 議員/议员, 国会議員/國會議員, 太平紳士/太平绅士, 医学博士/醫學博士, 二世 and the job titles 社長/部長/課長 and the simplified 社长/部长/课长 (Derek, 2026-10-17: the characters are shared and the offices are real in both languages; no borne-name data either way, #707) join `SUFFIX_WORDS`; none joins `GLUED_HONORIFICS`. The spaced bar is only that a word standing alone is no name, so they ship; the glued bar ("can it END a name?") needs real-name counts that this change did not get — the Wikidata suffix query timed out and the native-script Wikidata corpus holds none of the words — and 部長 fails it on argument alone (長 is a borne given name, 阿部長). A follow-up can vet 医学博士, 議員 and 太平紳士 one at a time. Consequence measured on `parser_for(locales.ZH)`: after a listed surname each word divides off, `王二世` family 王, suffix 二世 (it read given 二世) and `李部長` family 李, suffix 部長, because the segmenter's division is not a peel and the split-off 二世 is then a whole-token suffix word, the way 王博士 already reads; `陳怡君二世` still reads given 怡君二世 (glued, nothing divides it). ジュニア was already shipped by #661, and #679 declined 二世 as CLDR's generation word ("regnal, and glued"); here it ships spaced-only as the Chinese/Japanese numeral, which does not reverse that. Excluded as the issue stated: 小 (a prefix and a common given-name character) and 冯/馮 for "von" (a top-30 surname).

Excluded (the never-given / ambiguous particle line, nameparser/config/particles.py — #360 owns the vocabulary question):

Expand Down
17 changes: 17 additions & 0 deletions docs/design/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,8 @@ S2. Rationale: generational suffixes and credentials are recognized
"Агриппина Младшая" → suffix="Младшая"
"جورج بوش الابن" → suffix="الابن"
"ロバート・ダウニー・ジュニア" → suffix="ジュニア"
"佐藤恵子 国会議員" → suffix="国会議員"
"陳子媚 二世" → suffix="二世"
"Питер Брейгель мл." → suffix="мл."
"Marie Fils" → family="Fils" · boundary
"Jiří Dienstbier ml." → family="ml." · boundary
Expand Down Expand Up @@ -2362,6 +2364,21 @@ W2. Rationale: some East Asian honorifics glue directly onto the end
"김지양" → suffix="" · boundary
"선생님" → family="선생님" · boundary
"王君" → family="王君" · boundary
"田中 社長" → suffix="社長"
"田中部長" → family="田中部長" · boundary
"陳怡君二世" → family="陳怡君二世" · boundary
Accepted: the credential, office and generational words of #671
(議員, 国会議員, 太平紳士, 医学博士, 二世, 社長, 部長, 課長 and their
variant spellings) are vocabulary in the spaced and after-comma
positions only and no peel splits them off a glued name:
陳怡君二世 and 田中部長 keep the word in the name. The glued bar
is "can it END a name?", and none of these was measured against
real names (部長 fails it outright, 阿部長 being 阿部 + 長). The
Han segmenter's own division is not a peel and still exposes
them: under the Chinese pack a listed surname followed by one of
the words divides there and the word reads as a suffix, so
王二世 reads family 王, suffix 二世 and 李部長 family 李, suffix
部長, as 王博士 already does.
history: decisions.md#W2 · interacts: W3 · implemented: nameparser/_pipeline/_script_segment.py

W3. Rationale: a family name declared by a comma is the writer's
Expand Down
1 change: 1 addition & 0 deletions docs/release_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Release Log

- **Add junior and senior in other languages as suffixes.** ``Antonio Pavlović mlađi`` gives last ``Pavlović``, suffix ``mlađi``, where 1.4.0 through 2.4.2 gave middle ``Pavlović``, last ``mlađi``. The words come from 45 languages and are read when written as a separate word after the name. :issue:`661`, :issue:`679`
- **Remove mai from the default post-nominal acronyms, so a trailing Mai keeps the name.** ``Tran Thi Mai`` read in Vietnamese order (``FAMILY_FIRST_GIVEN_LAST``) gives given ``Mai``, middle ``Thi``, family ``Tran``, where 2.0.0 through 2.4.2 gave suffix ``Mai`` and no given name; ``Nguyen Mai`` gives family ``Mai``, as 1.4.0 did. The cost is that ``John Smith MAI`` gives last ``MAI`` instead of suffix; ``Smith, John, MAI`` and ``John Smith M.A.I.`` still read as credentials. A caller who needs it back adds it: ``Lexicon.default().add(suffix_acronyms={"mai"})``. See the ``suffix-acronym-collisions`` entry of ``docs/design/decisions.md`` (:issue:`687`)
- **Add Chinese and Japanese credential, office and generational words as suffixes.** ``彭德威 议员`` under ``locales.ZH`` gives family ``彭``, given ``德威``, suffix ``议员``, where 2.1 through 2.4.2 gave middle ``议员``; ``王雅婷,議員`` gives suffix ``議員``, where it gave given ``議員``. Read when written as a separate word or after a comma; no peel splits them off a glued name (``陳怡君二世`` is unchanged), though the Chinese pack's surname split exposes them after a listed surname (``王二世``). :issue:`671`
- **Add the Akan and Indonesian salutations owura and bapak as titles.** ``Bapak Dwi Putro`` gives title ``Bapak``, first ``Dwi``, last ``Putro``, where 1.4.0 through 2.4.2 gave first ``Bapak``, middle ``Dwi``. :issue:`662`

* 2.4.2 - October 13, 2026
Expand Down
14 changes: 14 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,20 @@ Exactly one honorific peels off a token, and the entries are whole
honorifics rather than parts: ``김민준박사님`` gives up 박사님 entire,
not 님 with 박사 left behind.

Credential and office words (議員 legislator, 医学博士 doctor of
medicine, 社長 company president, 二世 the second) are recognized in the
spaced form and after a comma only, and are not peeled when glued,
unlike さん. Nobody has checked them against names that end in them, so
the glued form is left whole:

.. doctest::

>>> shacho = parse("田中 社長")
>>> shacho.family, shacho.suffix
('田中', '社長')
>>> parse("田中社長").family
'田中社長'

When a division was a judgment call
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
29 changes: 29 additions & 0 deletions nameparser/config/suffixes.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,35 @@
'さま', # ja the kana spelling of 様
'くん', # ja the kana spelling of 君
'ちゃん', # ja familiar/diminutive
# #671: CJK credential, office and generational words. SPACED
# (and after-comma) ONLY -- none is in GLUED_HONORIFICS below.
# The spaced position is a token boundary the writer drew, so
# the vetting bar is just "is a word standing alone ever a
# name?"; the glued bar ("can it END a name?") needs real-name
# data this set was not measured against (decisions.md#W2, #671),
# so no peel splits them off a glued name (陳怡君二世, 田中社長
# keep the word) and glued peeling is a separate question (#707).
# The Chinese pack's SEGMENTER is not a peel and still exposes
# them after a listed surname: 王二世 and 李部長 divide at the
# surname and the split-off word is then a whole-token suffix
# word, as 王博士 already reads.
# - 二世: Japanese/Chinese "II" (the generational numeral).
# - 議員/议员, 国会議員/國會議員: legislator, Diet member.
# - 太平紳士/太平绅士: Justice of the Peace (Hong Kong).
# - 医学博士/醫學博士: Doctor of Medicine.
# - 社長/部長/課長 and the simplified 社长/部长/课长: job titles
# used as forms of address after a name (田中 社長, 李部长),
# not credentials -- the same slot as 先生. Japanese president,
# department head, section chief; Chinese society head,
# minister, section chief. The simplified 长 never occurs in
# Japanese text, so those entries touch no Japanese name.
# 部長 is the reason glued is out: 長 alone is a borne given
# name (阿部長 = 阿部 + 長).
'二世',
'議員', '议员', '国会議員', '國會議員',
'太平紳士', '太平绅士',
'医学博士', '醫學博士',
'社長', '部長', '課長', '社长', '部长', '课长',
# #346/#344/#343: spaced trailing honorifics, South Asian and
# Tibetan. SPACED ONLY -- none of these is in GLUED_HONORIFICS
# below, and none may be added to it. जी is the reason and the
Expand Down
55 changes: 55 additions & 0 deletions tests/v2/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -11475,6 +11475,61 @@ def _check_cjk_shape_purity(self) -> None:
"Hiroshi, an attested given name, so 博士 never peels "
"-- it stays recognized in the SPACED position only, "
"where the writer's own token boundary settles it"),
Case("zh_spaced_legislator_is_a_suffix", "彭德威 议员",
{"family": "彭", "given": "德威", "suffix": "议员"},
locale="zh", classification="fix(#671)",
notes="2.4.2 read middle '议员'; the spaced word is a token "
"the writer drew, so it needs only to be no name"),
Case("ja_spaced_diet_member_is_a_suffix", "佐藤恵子 国会議員",
{"family": "佐藤恵子", "suffix": "国会議員"},
classification="fix(#671)",
notes="2.4.2 read given '国会議員'. The unspaced twin "
"佐藤恵子国会議員 is unchanged (glued, nothing peels it); "
"family-first comes from the wholly-Han script order"),
Case("zh_comma_legislator_is_a_suffix", "王雅婷,議員",
{"family": "王雅婷", "suffix": "議員"},
classification="fix(#671)", tolerated=True,
notes="the fullwidth comma is a listing convention (W3, "
"best-effort); the word is vocabulary, so C1 reads the "
"part after the comma as a credential run rather than "
"reversing the name -- 2.4.2 gave it as the given name"),
Case("zh_comma_degree_is_a_suffix", "陳怡君,醫學博士",
{"family": "陳怡君", "suffix": "醫學博士"},
classification="fix(#671)", tolerated=True,
notes="a four-character degree word that is whole after the "
"comma, so the glued-博士 exclusion never applies"),
Case("zh_spaced_generational_two_is_a_suffix", "陳怡君 二世",
{"family": "陳", "given": "怡君", "suffix": "二世"},
locale="zh", classification="fix(#671)",
notes="2.4.2 read middle '二世'"),
Case("ja_spaced_job_title_is_a_suffix", "田中 社長",
{"family": "田中", "suffix": "社長"},
classification="fix(#671)",
notes="a form of address after the name, the slot 先生 "
"fills; a job title, not a credential"),
Case("ja_glued_job_title_stays", "田中部長",
{"family": "田中部長"},
classification="fix(#271)",
notes="the glued bar is unmet: 長 alone is a borne given "
"name (阿部長 = 阿部 + 長), so 部長 is spaced-only. "
"Unchanged from 2.4.2, pinned so GLUED_HONORIFICS "
"cannot gain it unseen"),
Case("zh_listed_surname_then_new_word_divides", "李部長",
{"family": "李", "suffix": "部長"},
locale="zh", classification="fix(#671)",
notes="the segmenter divides at the listed surname and the "
"split-off word is a whole-token suffix word, as 王博士 "
"already reads; 2.4.2 read given '部長'. 阿部長 stays "
"whole because 阿 is no listed surname"),
Case("zh_glued_generational_two_is_not_peeled", "陳怡君二世",
{"family": "陳", "given": "怡君二世"},
locale="zh", classification="fix(#271)",
notes="the glued form #671 left open: no peel reads it, "
"and 二世 is not vetted against real names. The "
"SEGMENTER's division is another matter: after a "
"listed surname the pack reads 王二世 as family 王, "
"suffix 二世, and 李部長 likewise, because the "
"split-off piece is a whole-token suffix word"),
Case("zh_glued_jun_stays", "王君",
{"family": "王君"},
classification="fix(#271)",
Expand Down
31 changes: 31 additions & 0 deletions tests/v2/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2103,3 +2103,34 @@ def test_a_decomposed_dotted_letter_is_dotted() -> None:
differ), so it is pinned directly."""
from nameparser._pipeline._vocab import _dotted
assert _dotted(unicodedata.normalize("NFD", "Ž.")) is True


# #671: the credential, office and generational words, one spelling each
# per distinct entry. They are spaced / after-comma vocabulary only; the
# glued bar was not met (#707 revisits it), so none may become a glued
# honorific without this list being edited on purpose.
_CJK_SPACED_WORDS = (
"議員", "议员", "国会議員", "國會議員", "太平紳士", "太平绅士",
"医学博士", "醫學博士", "二世", "社長", "部長", "課長",
"社长", "部长", "课长",
)


@pytest.mark.parametrize("word", _CJK_SPACED_WORDS)
def test_a_cjk_credential_word_is_a_suffix_spaced_and_after_a_comma(
word: str) -> None:
parser = Parser()
for text in (f"田中 {word}", f"田中, {word}", f"Tanaka, {word}"):
parsed = parser.parse(text)
assert parsed.suffix == word, text
assert word not in parsed.family + parsed.given, text
# a one-word head and an empty head (CSV data with the surname
# missing) are the comma paths that have disagreed before
assert parser.parse(f", {word}").suffix == word


@pytest.mark.parametrize("word", _CJK_SPACED_WORDS)
def test_a_cjk_credential_word_does_not_peel_off_a_glued_name(
word: str) -> None:
assert word not in Parser().lexicon.honorific_tails
assert Parser().parse(f"田中{word}").suffix == ""
24 changes: 12 additions & 12 deletions tools/differential/cldr_agreement.json
Original file line number Diff line number Diff line change
Expand Up @@ -13086,11 +13086,11 @@
},
"given_names": {
"expected": "子媚 惠儀",
"got": "太平紳士"
"got": ""
},
"suffix": {
"expected": "小 太平紳士",
"got": ""
"got": "太平紳士"
},
"text": "先生子媚惠儀陳小,太平紳士",
"title": {
Expand Down Expand Up @@ -13344,11 +13344,11 @@
},
"given_names": {
"expected": "子媚 惠仪",
"got": "太平绅士"
"got": ""
},
"suffix": {
"expected": "小 太平绅士",
"got": ""
"got": "太平绅士"
},
"text": "先生子媚惠仪陈小,太平绅士",
"title": {
Expand Down Expand Up @@ -13758,23 +13758,23 @@
"zh_Hant/foreignFull/givenFirst": {
"family": {
"expected": "馮 陳",
"got": "怡君達印馮 陳二世博士"
"got": "陳二世博士"
},
"family_base": {
"expected": "陳",
"got": "怡君達印馮 陳二世博士"
"got": "陳二世博士"
},
"family_particles": {
"expected": "馮",
"got": ""
},
"given_names": {
"expected": "怡君 達印",
"got": "醫學博士"
"got": "怡君達印馮"
},
"suffix": {
"expected": "二世 醫學博士",
"got": ""
"got": "醫學博士"
},
"text": "怡君達印馮·陳二世博士,醫學博士",
"title": {
Expand Down Expand Up @@ -13886,19 +13886,19 @@
"zh_Hant/nativeFull/givenFirst": {
"family": {
"expected": "王",
"got": "雅婷婷婷王二世先生"
"got": "雅婷婷婷王二世"
},
"family_base": {
"expected": "王",
"got": "雅婷婷婷王二世先生"
"got": "雅婷婷婷王二世"
},
"given_names": {
"expected": "雅婷 婷婷",
"got": "議員"
"got": ""
},
"suffix": {
"expected": "二世 議員",
"got": ""
"got": "先生, 議員"
},
"text": "雅婷婷婷王二世先生,議員",
"title": {
Expand Down
24 changes: 12 additions & 12 deletions tools/differential/cldr_agreement_locales.json
Original file line number Diff line number Diff line change
Expand Up @@ -617,11 +617,11 @@
},
"given_names": {
"expected": "子媚 惠儀",
"got": "太平紳士"
"got": ""
},
"suffix": {
"expected": "小 太平紳士",
"got": ""
"got": "太平紳士"
},
"text": "先生子媚惠儀陳小,太平紳士",
"title": {
Expand Down Expand Up @@ -816,11 +816,11 @@
},
"given_names": {
"expected": "子媚 惠仪",
"got": "太平绅士"
"got": ""
},
"suffix": {
"expected": "小 太平绅士",
"got": ""
"got": "太平绅士"
},
"text": "先生子媚惠仪陈小,太平绅士",
"title": {
Expand Down Expand Up @@ -1112,23 +1112,23 @@
"zh_Hant/foreignFull/givenFirst": {
"family": {
"expected": "馮 陳",
"got": "怡君達印馮 陳二世博士"
"got": "陳二世博士"
},
"family_base": {
"expected": "陳",
"got": "怡君達印馮 陳二世博士"
"got": "陳二世博士"
},
"family_particles": {
"expected": "馮",
"got": ""
},
"given_names": {
"expected": "怡君 達印",
"got": "醫學博士"
"got": "怡君達印馮"
},
"suffix": {
"expected": "二世 醫學博士",
"got": ""
"got": "醫學博士"
},
"text": "怡君達印馮·陳二世博士,醫學博士",
"title": {
Expand Down Expand Up @@ -1240,19 +1240,19 @@
"zh_Hant/nativeFull/givenFirst": {
"family": {
"expected": "王",
"got": "雅婷婷婷王二世先生"
"got": "雅婷婷婷王二世"
},
"family_base": {
"expected": "王",
"got": "雅婷婷婷王二世先生"
"got": "雅婷婷婷王二世"
},
"given_names": {
"expected": "雅婷 婷婷",
"got": "議員"
"got": ""
},
"suffix": {
"expected": "二世 議員",
"got": ""
"got": "先生, 議員"
},
"text": "雅婷婷婷王二世先生,議員",
"title": {
Expand Down
Loading
Loading