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
19 changes: 19 additions & 0 deletions docs/design/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,24 @@ The mechanism shipped twice before anyone wrote down its criterion. Sizes as of

Open: [#348](https://github.com/derek73/python-nameparser/issues/348) applying C-i to the 711 title entries, then titles_ambiguous plus a TITLE_OR_GIVEN kind. Blocked on data, not on judgement — the census needs a given-name frequency corpus this repo does not have, which is why the criterion is recorded here and the census is not attempted.

### v1-xfail-triage — the eight inherited aspirations, four retired and four kept

The reconciled v1-style banks (`tests/test_*.py`) carried eight `@pytest.mark.xfail` tests inherited from v1, each an aspiration nobody had judged since. `xfail_strict = true`, so all eight were live claims that 2.0 still does not do the thing — but a bare marker says only that, never whether the thing is WANTED, and that is the gap this triage closes. After it, every surviving marker cites an issue and every retired one is a deliberate pin of current behavior. The parses quoted below are this entry's evidence and its own recompute:

uv run python -c "from nameparser import HumanName; [print(repr(s), repr(HumanName(s))) for s in ('Maier, Amy I, Jr.', 'Dr King Jr', 'King, Dr Jr', 'Ahmad ben Husain', \"The Right Hon. the President of the Queen's Bench Division\")]"

- 2026-09-01 (Derek's triage) — NOT FIXED, four. The aspiration is rejected, the marker is gone, and the test now pins what the parser does with the grounds recorded beside it so nobody re-derives the rejected proposal:
- `Maier, Amy I, Jr.` — v1 wanted suffix "I, Jr."; it reads given Amy, middle I, family Maier, suffix "Jr.". A middle initial `I` is far more common than an ordinal I borne without a Sr./Jr.-style companion, so where an explicit suffix comma has already named the suffix, the trailing I stays a middle initial. That is #vocabulary-collisions' commonality reasoning applied to a SHAPE rather than to a word — the criterion is written per-word, and the extension is named here rather than smuggled in. **This amends rules.md#C1 in the same diff.** C1's one-character clause said a bare single letter behind a name word is the generation, full stop, which predicts exactly the suffix "I, Jr." being rejected here. The discriminator the parser has always applied and C1 omitted is the FURTHER COMMA: `Maier, Amy I` reads suffix 'I' while `Maier, Amy I, Jr.` reads middle 'I', and `Smith, John V` against `Smith, John V, Jr.` is the same pair on a name C1 already exemplifies — all four measured. So the rule was incomplete, not the behavior: a rules.md defect rather than a parser one, which is the call this file already made for `'Donald mc'` in the fields-only arc entry (2026-08-27, #451). The qualifier is normative prose carrying no example line of its own; C1's Accepted block records why and names the bank pair as its executable witness.
- `Dr King Jr` — v1 wanted title 'Dr', family 'King', suffix 'Jr'; it reads title 'Dr King', family 'Jr'. `king` stays in the titles vocabulary: it is there for the addressing forms ("King Charles"), and taking it out to serve the surname reading trades a common use for a rarer one, which is the direction #vocabulary-collisions cuts. TITLES has no ambiguous subset and no AmbiguityKind, so — as with MAIDEN_MARKERS and `roz` — the only two expressions of the criterion available here are ship and do not ship; #348 is the open work that would give this set a third answer. The comma format is the road to the surname reading, and is now pinned alongside rather than left as prose — `King, Dr Jr` reads title 'Dr', family 'King', suffix 'Jr'. The test cites [#27](https://github.com/derek73/python-nameparser/issues/27), which is closed; this is the decision it never got.
Two halves, and only one is decided. DECIDED: `king` stays in TITLES. RECORDED, not endorsed: what becomes of the leftover `Jr`. rules.md#S2 predicts suffix 'Jr' with an empty family — its Accepted clause consumes an unambiguous suffix even when nothing is left to be the family (`Smith Jr.` → family "") — but once the title chain has taken `Dr King`, H1 claims the one remaining word and it reads family 'Jr', suffix ''. `Dr Smith Jr` isolates the cause: family 'Smith', suffix 'Jr', exactly as S2 states. S2 now carries a descriptive note saying so. A future change moving `Dr King Jr` toward S2's prediction is an IMPROVEMENT and updates the pin; it is not a regression, and the test says as much so nobody reads the pin as an endorsement.
- `Ahmad ben Husain` — v1 wanted family "ben Husain"; it reads given Ahmad, middle ben, family Husain. Already decided in v0.2.5, when `ben` came out of the prefixes, and for the reason that still holds: `ben` collides with the given name Ben, in the position the particle claim would act on — `Ahmad Ben Husain` reads middle 'Ben' today, which is exactly the token a forward-joining particle claim would take. That is C-i's position test, and it keeps `ben` out. Recorded a second time as a standing keep-out in this file's Excluded block for the particle set, because that is where a wordlist sweep meets it: a keep-out that lives only in a triage entry is one the next Arabic/Hebrew patronymic-particle sweep never reads. Worth naming as a failure mode of its own — the marker was an aspiration that outlived its own resolution, and nothing in a bare xfail says which of the eight were like that.
- `The Right Hon. the President of the Queen's Bench Division` — v1 wanted the whole string as one title; it reads title "The Right Hon. the President of the Queen's Bench", family 'Division'. This is a name parser, not a title parser: handed an input that is all titles it assumes the last title-word is the name. Accepted as convention rather than defended as correct — what is actually wrong is that the guess is silent, which is [#491](https://github.com/derek73/python-nameparser/issues/491), not this reading.
- 2026-09-01 — FIX CANDIDATES, four. The marker stays and now carries its issue, so `pytest -rx` names the work instead of listing anonymous aspirations:
- [#489](https://github.com/derek73/python-nameparser/issues/489) — `Her Majesty Queen Elizabeth` should address by given name (`tests/test_conjunctions.py::test_conjunction_in_an_address_with_a_first_name_title`).
- [#490](https://github.com/derek73/python-nameparser/issues/490) — `E.T. Smith` (`tests/test_conjunctions.py::test_two_initials_conflict_with_conjunction`) and `U.S. District Judge Marc Thomas Treadwell` (`tests/test_titles.py::test_chained_title_first_name_title_is_initials`). One issue for two tests deliberately: each test's own comment names the other's shape as what blocks a fix — dotted initials against dotted title and credential vocabulary — so they are one question, and fixing either alone is what has failed before.
- [#492](https://github.com/derek73/python-nameparser/issues/492) — `capitalize()` leaves `juan garcia III` lowercase (`tests/test_capitalization.py::test_capitalization_exception_for_already_capitalized_III_KNOWN_FAILURE`). The `_KNOWN_FAILURE` suffix is kept: it is still true, and beside the annotated reason it reads as redundant rather than misleading.
- [#485](https://github.com/derek73/python-nameparser/issues/485) is superseded. It proposed prefixing corpus labels with `xfail:` so a radar diff on a name like `Dr King Jr` reads as a known-bad parse improving rather than a regression. The premise was that a v1 xfail marker is a usable triage signal; after this triage it is not, because the marker no longer tracks the disposition — `Dr King Jr` is now a PIN, so a radar diff on it means a decided reading moved, which is precisely the signal the prefix would have suppressed. The four that remain carry issue numbers, which is the same information in a place that cannot go stale against the pinned historical ref.

### suffix-field-composition — three kinds of thing in one field

- 2026-08-16 (suffix keystone; #326) — measured composition of suffix_words (40 entries): 11 generational (i, ii, iii, iv, v, jr, jnr, sr, snr, junior, 2), 5 neither (dr, esq, esquire, ret, vet), and 24 POSTNOMINAL HONORIFICS — 20 CJK (さん, さま, くん, ちゃん, 様, 殿, 氏, 先生, 博士, 教授, 女士, 小姐, 씨, 양, 군, 님, 박사, 박사님, 교수님, 선생님) and 4 Hebrew (ז"ל, ז״ל, שליט"א, שליט״א). The honorifics are the LARGEST group.
Expand Down Expand Up @@ -389,6 +407,7 @@ Excluded (the never-given / ambiguous particle line, nameparser/config/particles

- Only 9 of the ambiguous members were ever individually justified; the rest sit there by the conservative default (ambiguous unless argued never-given).
- mc, ste — measured misparses ("Mc Donald" → given "Mc"), tracked in #360; st is inert at the head because TITLES claims it first; mac must stay ambiguous because Mac is a real given name.
- ben — OUT of the set entirely, removed in v0.2.5 and re-affirmed 2026-09-01 (#v1-xfail-triage). Ben is a common given name standing exactly where a forward-joining particle claim would act: "Ahmad Ben Husain" reads middle 'Ben' today, and admitting `ben` would take that token into the family. This is the C-i position test, and the entry is here because the pressure to re-add is real and recurring — "Ahmad ben Husain" wants family "ben Husain", the v1 suite carried that aspiration as an xfail for years, and a sweep of Arabic and Hebrew patronymic particles is exactly how it comes back. A caller who needs the patronymic reading adds it to their own Lexicon.
- Encoding rationale (#293, predating #360's membership questions): the data layer stores the NEVER-GIVEN set and derives the ambiguous one, because that is safe-by-default for new particles — a one-place addition — and the v1 shim translates by one-directional complement. And the constants are FROZEN specifically to kill the cached-Lexicon.default()-vs-fresh- Constants desync that runtime module-constant mutation caused.
- Load-bearing dependency: TITLES ∩ ambiguous — {do, freiherr, st} until #296's audit, {freiherr, st} since — is what keeps the particle-or-given ambiguity emitter reachable at all; moving them all would leave the shipped vocabulary unable to reach it, which is why test_the_chained_emitter_is_reachable_by_construction (tests/v2/test_parser.py) pins reachability by construction rather than by shipped vocabulary — an empty intersection fails no test and does not mean the emitter is unreachable.

Expand Down
44 changes: 40 additions & 4 deletions docs/design/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,22 @@ S2. Rationale: generational suffixes and credentials are recognized
"Jack Wei Ma" → suffix="Ma"
"Jack Wei Ma" → ambiguities=("suffix-or-name",)
"Smith Jr." → family=""
implemented: nameparser/_pipeline/_classify.py, nameparser/_pipeline/_group.py, nameparser/_pipeline/_pieces.py, nameparser/_pipeline/_vocab.py
Note, DESCRIPTIVE and not promised: where a title chain consumes
every word but one, the word left over is claimed by H1's
one-word reading before this rule's trailing-suffix reading
reaches it — as the family ordinarily, as the given name behind
a given-name title (`Sir Jr` reads given `Jr`). `Dr King
Jr` reads title `Dr King`, family `Jr`, empty suffix — not the
suffix `Jr` with an empty family the Accepted clause above
predicts — because `king` is title vocabulary and H1 then takes
the one remaining word. The contrast that isolates the cause is
`Dr Smith Jr`, which reads family `Smith`, suffix `Jr` as
stated. Only the vocabulary half is decided
(decisions.md#v1-xfail-triage: `king` stays a title, for the
addressing forms); the leftover reading is recorded as today's
rather than endorsed, and a change moving it toward this rule's
prediction is an improvement, to be argued here.
interacts: H1, H2, C1 · implemented: nameparser/_pipeline/_classify.py, nameparser/_pipeline/_group.py, nameparser/_pipeline/_pieces.py, nameparser/_pipeline/_vocab.py

S3. Rationale: credentials are often written run together with
periods; the chunks between the periods are what carry the
Expand Down Expand Up @@ -836,9 +851,17 @@ C1. Rationale: a credential run after the comma means the name is in
run, written with a period or without, an initial being no shape
anyone writes there; behind a name word it is the generation
only when written bare, a period marking it the abbreviation of
a name and so a middle initial. Longer suffix words are not in
question either way, and the strict knob above still vetoes the
initial-shaped ones, so the run ends at them there.
a name and so a middle initial. Both branches then ask that the
generation slot still be open: where a further comma has already
named the suffix, a single letter ending the GIVEN part has no
generation left to be, and stays a middle initial — with a
period or without, and whether a name word or another suffix
word stands before it. Only the given part is touched; a part
after the comma that is nothing but suffix words is the
credential run, and a letter in it continues that run up to the
further comma. Longer suffix words are not in question either
way, and the strict knob above still vetoes the initial-shaped
ones, so the run ends at them there.
"Smith, John" → family="Smith"
"سلمان، محمد" → family="سلمان"
"田中、太郎" → family=""
Expand Down Expand Up @@ -885,6 +908,19 @@ C1. Rationale: a credential run after the comma means the name is in
form alone, and that limitation is kept as parity: "Smith, RN -
CRNA" reads given "RN" under the policy as without it.
"John Smith, LEED AP" → family="Smith" deviates: #291 (today: family="John Smith")
Accepted: the further-comma qualifier carries no example line of
its own. It discriminates PAIRS and spans both branches, so
exemplifying it means a with-comma partner for each — every one
of them a name entering the rules corpus for behavior that has
not moved since v1. Its executable witness is instead the pair
already standing in the v1-style bank,
tests/test_suffixes.py's
test_roman_numeral_i_after_single_initial_lastname_comma_format
and test_roman_numeral_i_with_explicit_suffix_comma_stays_a_middle_initial.
Read the examples above with the qualifier in hand: `Smith, John
V` reads the suffix and `Smith, John PhD I.` continues the run,
while adding a suffix comma after either turns that same letter
into the middle initial.
history: decisions.md#C1 · interacts: H2, P6, W3 · implemented: nameparser/_pipeline/_segment.py, nameparser/_pipeline/_assign.py, nameparser/_pipeline/_group.py

C2. Rationale: text beyond the recognized comma parts should be
Expand Down
2 changes: 1 addition & 1 deletion tests/test_capitalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def test_capitalization_exception_for_III(self) -> None:

# FIXME: this test does not pass due to a known issue
# http://code.google.com/p/python-nameparser/issues/detail?id=22
@pytest.mark.xfail
@pytest.mark.xfail(reason="#492")
def test_capitalization_exception_for_already_capitalized_III_KNOWN_FAILURE(self) -> None:
hn = HumanName('juan garcia III')
hn.capitalize()
Expand Down
4 changes: 2 additions & 2 deletions tests/test_conjunctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def test_lowercase_middle_initial_comma_lastname_and_suffix_conflict_with_conjun
self.m(hn.last, "Smith", hn)
self.m(hn.suffix, "III, Jr", hn)

@pytest.mark.xfail
@pytest.mark.xfail(reason="#490")
def test_two_initials_conflict_with_conjunction(self) -> None:
# Supporting this seems to screw up titles with periods in them like M.B.A.
hn = HumanName('E.T. Smith')
Expand Down Expand Up @@ -213,7 +213,7 @@ def test_conjunction_in_an_address_with_a_title(self) -> None:
self.m(hn.title, "His Excellency Lord", hn)
self.m(hn.last, "Duncan", hn)

@pytest.mark.xfail
@pytest.mark.xfail(reason="#489")
def test_conjunction_in_an_address_with_a_first_name_title(self) -> None:
hn = HumanName("Her Majesty Queen Elizabeth")
self.m(hn.title, "Her Majesty Queen", hn)
Expand Down
Loading