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
4 changes: 2 additions & 2 deletions AGENTS.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/design/decisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Entry conventions:
- [S7 — the company of an unambiguous credential](S7-company-of-an-unambiguous-credential.md)
- [S8 — the credential's trailing run](S8-credentials-trailing-run.md)
- [S9 — unlisted words that join the ambiguous class by shape](S9-unlisted-words-that-join-the-ambiguous-class-by-shape.md)
- [S10 — the trailing roman numeral](S10-trailing-roman-numeral.md)

## Nicknames & quoted names (N)

Expand Down
12 changes: 12 additions & 0 deletions docs/design/decisions/S10-trailing-roman-numeral.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# S10 — the trailing roman numeral

- 2026-10-17 (Derek), #718 — A ONE-LETTER NUMERAL NEEDS WORDS TO SPARE, AND THE RULE IS WRITTEN DOWN. Until this entry no rule stated the comma-less numeral at all: the behavior was pinned in tests/v2/cases.py and credited in code comments to rules/S2, whose statement covers the suffix vocabulary and never the shape fork that reads `VI` to `X`, nor the initial veto that sends `V`, `I` and `X` there. The extraction found one reading worth changing. `John V` and `Malcolm X` read suffix with an EMPTY family in every 2.x release (2.0.0 through 2.4.2 measured), alongside 2.0.0's `Johnson PhD` fix; 1.4.0 read family `V` and `X`. Derek: a generational V is mostly archaic, X rarer still and I hardly ever written without a comma (Sr. and Senior are what people write), so a capital letter standing alone behind the given name is far likelier an initial or a one-letter surname. The numeral was never what S2's Accepted clause (an unambiguous suffix is consumed even when that leaves no family) covers, since the fork has always reported itself ambiguous; S4's count is the analogue, and the numeral now asks it.
WHAT MOVES. The count is S4's own, in name UNITS, so a particle chain is one word (P2) and a title none. Measured on the shipped change against origin/master at 958d252e, the snapshot moves four corpus names: `John V` and `Mohamad X` (family V and X, as 1.4.0 read them), `John née Jones Smith V` (the clause-free name is `John V`, so the V is no suffix and the clause, which M2's statement takes up to the trailing run the clause-free name reads, keeps it: maiden 'Jones Smith V'), and `Freiherr von Richthofen V`, family 'von Richthofen V': behind a title the chain is the one name word, so the V has no word to spare and the chain takes it as P2 takes any name word, as it already took `Freiherr von Richthofen Ma`. That last is the reading a scratch prototype counting raw pieces did not show, and the one most worth a second look. Four case rows moved with it (`strict_comma_reads_the_bare_numeral_into_the_run`: `Smith V, Ma` under the strict knob reads family V, suffix Ma, as 1.4.0 did; and the rows whose purpose needed a suffix kept their ids on a spelling that still has one — `abdul Doe nee Smith V`, `John III, Dr.`, `John III, Sir` — with the old inputs re-pinned as new rows: `abdul nee Smith V` maiden 'Smith V', `John V, Dr.` and `John V, Sir` given John, family V). `Sir abdul V` reads given 'abdul V': the V is a name word, and P5's licence behind a given-name title joins it. Nothing with a word to spare moves: `John W. Ingram V`, `King John Alexander V`, `Larry James Johnson I` (a v1 test) keep the suffix and the report. A title is no word to spare, so `Dr Smith V` reads given Smith, family V, which is 1.4.0's reading too, where `Dr. von Richthofen V` keeps suffix V, the particle and the surname being two words there.
THE LOWERCASE HALF. A numeral in lower case in a mixed-case name is not how anyone writes an initial, so it is the generation and reports nothing (Derek). Measured before the change, the reports had no rationale: `John Smith v` and `John Smith i` were silent because the listed letters reach the vocabulary path, `John Smith vi` and `John Smith x` reported because the shape fork reports everything it takes, and the one-case `john smith i` reported because the connective tag (P3, #479) makes a one-case `i` an initial. Now the mixed-case lower case is uniformly silent and a one-case name is counted like capitals, `v` excepted (the ACCEPTED paragraph below). `john smith i` keeps its report, by Derek's call.
Declined: dropping the shape fork for single capital letters outright. Prototyped in a scratch edit, it moved 26 readings and cost the real generational names the count keeps: `John W. Ingram V` read middle 'W. Ingram', family 'V', and `John van der Berg V` folded the numeral into the particle chain. A variant that dropped the fork for I and X only, on top of the count, moved four more, among them the v1 test `Larry James Johnson I`.
ACCEPTED, and recorded in rules/S10: a one-case lowercase `v` is read as the vocabulary reads it, so `john v` still loses its family and `john smith v` reports nothing, where `JOHN V` and `john x` keep their family and `JOHN SMITH V` reports — the listed lowercase letter is taken as a suffix word before the numeral fork is reached, and closing that means touching the suffix-vocabulary path, which is not this change. Not `i`, though it is listed too: a one-case `i` is P3's connective, which classify reads as an initial, so it reaches the fork and is counted (`john i` → family 'i'; `john smith i` reports). And a one-letter numeral with a credential behind it is not the last word and is read as a name word (`John Smith V PhD` → family V, 1.4.0 parity), where `John Smith III PhD` reads suffix 'III PhD'.
Recompute: the moved names are this commit's diff of tools/differential/snapshot.jsonl (or `uv run python tools/differential/snapshot.py` on the parent tree with this commit's code). The Declined paragraph's figures are a prototype's, over every cases.py row and snapshot name parsed before and after the patch in one process, on master at 958d252e: its 26 replace `_pieces.is_trailing_numeral_suffix` with a wrapper returning False for a text that is one capital `I`, `V` or `X` (periods stripped) and the original's answer otherwise; its "four more" apply that wrapper to `I` and `X` alone on top of the shipped count. Both are dated snapshots by Counting claims; nothing re-runs them.
- 2026-10-18 (Derek), #718 — THE DECLINED NUMERAL REPORTS TOO, BOTH DIRECTIONS AS S4'S FORK DOES. The count's first form reported only the generation, so `John V` read family V in silence while `Jack Ma` reports keeping its family; Derek: report both, as `Jack Ma` does. A declined numeral is a pick of the walk, and its branches are taken in three places, so three sites report it (mechanisms.md#AMBIGUITY-AT-THE-DECISION-SITE): assign where it stays a lone name word (`John V` — "read as a family name rather than a generational suffix"), the particle chain where it joins the run (`Freiherr von Richthofen V`), and a maiden clause that keeps it as its last word (`John née Jones Smith V`, `abdul nee Smith V`). Each wording is its own, since the acronym pick's ("both a post-nominal and an ordinary name") misdescribes a numeral.
ONE STAND-DOWN, FOUND BY INV10 rather than designed. In a one-case name the connective `i` reaches the fork as a letter classify has already reported as `conjunction-or-initial`, read as an initial, and the new report put a second report on the same token: 138 rows of the connective grid (measured on this branch with the `reported_as_initial` check out of assign's numeral report, a dated snapshot) in tests/v2/test_properties.py (`AND ROVIRA I`, `HENRY I`) tripped INV10's kind pair. The two reports did not contradict each other — both read the letter as a name word — but they named one reading twice, so the numeral's report stands down where the initial reading is already reported, the shape of rules/S7's "a member its own capitals already made the credential reports nothing new". The guard was left untouched: its kind pair still means "two readings" everywhere else, and narrowing it to suit this change would have retaken its recorded control. `HENRY I` therefore reports `conjunction-or-initial` alone, and moved in test_assemble's contrast list from the withdrawn side to the side where the fork resolved to a name word.
- 2026-10-18, #718 review — THE COUNT IS NOT ASKED IN THE GIVEN PART AFTER A FAMILY COMMA. The code review's grid (family-comma and empty-head shapes behind a title-particle, about 558,000 parses) found 2,880 parses where the first form moved a numeral from the suffix into the given or family name with no report: `Smith, Freiherr von Berg V` read given 'von Berg V', `, Freiherr von V` family 'von V', `Smith, St van V` given 'van V', where `Smith, von V` and `Smith, Freiherr John V` kept suffix V. The cause was not a choice: assign's walk over that part never counts the numeral (rules/S4: the given part's trailing slot "is read without it (S5)"), but the particle chain's bound there asks `tail_reading`, which the count had reached, so the chain took a word assign would have read as the suffix. The bound now reads with the count off (`trailing_run_start`, `count_numeral=False`), and all four read as they did on master (2.2.0 onward, since #424; 1.4.0 read the numeral into the name). The class is the one the AGENTS.md gotcha on group-side predictors of the given part names, met from the other side: the predictor did not change, the peel it asks did. The S4 analogue, `Smith, Freiherr von Berg Ma` → given 'von Berg Ma', is master's reading and is left alone here.
- 2026-10-18, #718's PR review — TWO OF THE THREE REPORT SITES ARE ONE, AND THE CLAUSE ASKS THE CASE. The test-coverage review mutated each new emitter out in a copy of the tree. Removing the particle chain's numeral report moved no field and no kind — assign's pick loop reported the same `suffix-or-name`, worded from the final role ('read as a family name') — so the chain's branch was only choosing a sentence, and it is gone: a declined numeral stays one of the read's picks, and assign alone reports it, the decision having been the read's and not the chain's. The maiden clause's stand-down on a letter already reported as an initial could not fire at all (classify reads no connective inside a clause), so that call is gone too. And the clause's report on a numeral its take gave up did not ask the case: `Jane Doe nee Smith vi` and `x` reported where every other path had gone silent, the drift mechanisms.md#ONE-PREDICATE-PER-QUESTION names; the take now asks `numeral_settled_by_case` as assign does, `VI` still reporting beside them. Five rows the first draft labelled `fix(#718)` read field for field as 1.4.0 does and are `parity`, as are the two Freiherr rows added here; a restored 1.4.0 reading is parity by the table's own definition (rules/S4's `bare_surname_is_not_a_credential` is the precedent).
5 changes: 3 additions & 2 deletions docs/design/rules/M2-maiden-marker-rule.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ M2. Rationale: a maiden marker announces that what follows it is the
"Jane Smith née Jones" → maiden="Jones"
"Maria López née García Pérez" → maiden="García Pérez"
"Jane Smith née Jones PhD" → suffix="PhD"
"John née Jones Smith V" → maiden="Jones Smith"
"John née Jones Smith V" → suffix="V"
"Jane Doe née Jones Smith V" → maiden="Jones Smith"
"Jane Doe née Jones Smith V" → suffix="V"
"John née Jones Smith V" → maiden="Jones Smith V" · boundary
"Doe nee Smith, Jane" → maiden="Smith"
"Doe, Jane nee Smith" → maiden=""
"Doe, Jane nee Smith" → middle="nee Smith"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ O5. Rationale: O4 reads a name by comparing where its words stand,
nothing after it names no family and silences nothing, so
`سلمان،` and `Smith,` report where `Smith, Andrew` does not. A
comma with only a TITLE after it names no family either, but the
title still decides the field wherever it stands, so `John V, Dr.`
title still decides the field wherever it stands, so `John III, Dr.`
is silent for H1's reason and not the comma's. A word with no
letter or digit in it is no name word and
reports nothing (A2). Where the one name unit carries title
Expand Down
10 changes: 5 additions & 5 deletions docs/design/rules/P2-particles-join-forward.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ P2. Rationale: a particle is written as part of the surname it
a trailing suffix begins — the trailing run S2 reads, once,
before the chain is made and counting the chain's own run as the
one name word it will be, so the chain cannot change what it
counts: a trailing roman numeral, or a
bare acronym with words to spare, ends the chain as a suffix word
does, and so does a bare ambiguous acronym written in capitals in
counts: a trailing roman numeral S10 reads as the generation, or
a bare acronym with words to spare, ends the chain as a suffix
word does, and so does a bare ambiguous acronym written in capitals in
a mixed-case name, which needs no words to spare; the same
acronym written Title-case in a mixed-case name ends nothing and
joins the chain as any name word does, words to spare or not,
Expand Down Expand Up @@ -48,7 +48,7 @@ P2. Rationale: a particle is written as part of the surname it
"John van der J. V" → family="van der J. V" · boundary
"Freiherr von Berg MA" → family="von Berg"
"Freiherr von Berg MA" → suffix="MA"
"Freiherr von Richthofen V" → suffix="V" · boundary
"Freiherr von Richthofen V" → family="von Richthofen V" · boundary
"SMITH VD MA, JOHN" → family="SMITH VD MA"
"SMITH VD, JOHN" → suffix="VD" · boundary
"John van der Berg née Jones" → family="van der Berg"
Expand Down Expand Up @@ -76,5 +76,5 @@ P2. Rationale: a particle is written as part of the surname it
(#132's ask) has it as the surnames view rather than the
family field.
"Vincent van Gogh van Beethoven" → surnames="van Gogh van Beethoven"
history: decisions/P2 · interacts: P1, P4, H5, M2, S2, C2, S4 · implemented: nameparser/_pipeline/_assign.py, nameparser/_pipeline/_group.py, nameparser/_pipeline/_pieces.py, nameparser/_pipeline/_post_rules.py
history: decisions/P2 · interacts: P1, P4, H5, M2, S2, C2, S4, S10 · implemented: nameparser/_pipeline/_assign.py, nameparser/_pipeline/_group.py, nameparser/_pipeline/_pieces.py, nameparser/_pipeline/_post_rules.py

2 changes: 1 addition & 1 deletion docs/design/rules/P3-connectives.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ P3. Rationale: connective words ("y", "of the") bind name words into
connectives standing between. A connective with nothing to its right is connecting
nothing, and a word of that vocabulary ending a name, or standing
before the credential a name ends with, is the generation it also
spells.
spells wherever S10 reads one there.
A separator the caller declared, standing in a trailing suffix
part, ends the search there as a comma would, so a connective
never joins across it (C1); anywhere else it is a word.
Expand Down
2 changes: 1 addition & 1 deletion docs/design/rules/P5-bound-given-names.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ P5. Rationale: some given-name words are incomplete alone — "abdul"
each read over what the other leaves until neither takes
anything more, are read once, before the join is tried, and the
name words they leave are the words to spare. A trailing
roman numeral, or a bare acronym the run takes, or a trailing
roman numeral the run takes (S10), or a bare acronym it takes, or a trailing
title word the run takes, is no
word to spare. The join joins two name words into one and, read
after the trailing run, changes no suffix reading: a word the
Expand Down
1 change: 1 addition & 0 deletions docs/design/rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ The marker's unit is the whole rule, so a clause that reaches past the tolerated
- [S7 — the company of an unambiguous credential](S7-company-of-an-unambiguous-credential.md)
- [S8 — the credential's trailing run](S8-credentials-trailing-run.md)
- [S9 — unlisted words that join the ambiguous class by shape](S9-unlisted-words-that-join-the-ambiguous-class-by-shape.md)
- [S10 — the trailing roman numeral](S10-trailing-roman-numeral.md)

## [Nicknames & quoted names (N)](N-nicknames-and-quoted-names.md)

Expand Down
63 changes: 63 additions & 0 deletions docs/design/rules/S10-trailing-roman-numeral.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# S10 — the trailing roman numeral

S10. Rationale: a roman numeral ending a name is its generation,
but one letter is also how an initial is written, and the
generation it would name is rare; a name of two words has no
word to spare for it.
A roman numeral ending a name written without a comma reads as
the generation — the numerals the vocabulary lists, and those
up to X it does not, by their shape alone — unless the word in
front of it is written as an initial, which makes it a name word
too. A single letter written with a period is an initial, and a
numeral past X is a name word. A numeral of one letter needs
words to spare, as a bare ambiguous acronym does (S4): as the
second of two name words it is the family name, a title in front
being no word to spare, and either reading is reported, as
either reading of an ambiguous acronym is. A numeral read by its
shape alone reports too; a numeral the vocabulary lists in two
or more letters is not in question and reports nothing, and a
letter already reported as read as an initial reports nothing
new. Written case
settles the fork in one way only: a numeral written in lower
case in a name of mixed case is the generation and reports
nothing, since nobody writes an initial that way. A name written
wholly in one case says nothing about any letter in it. The part
before a suffix comma is read as the same name written without
the comma (C1); the given part after a family comma is not
counted, the comma having settled the count there (S5).
"John Smith V" → suffix="V"
"John Smith V" → ambiguities=("suffix-or-name",)
"John W. Ingram V" → suffix="V"
"John Smith VI" → suffix="VI"
"John Smith VI" → ambiguities=("suffix-or-name",)
"John Smith III" → ambiguities=()
"John III" → suffix="III"
"John V" → family="V"
"John V" → ambiguities=("suffix-or-name",)
"Malcolm X" → family="X"
"JOHN V" → family="V"
"Dr Smith V" → family="V"
"John V, Dr." → family="V"
"John Smith vi" → ambiguities=()
"john smith i" → ambiguities=("suffix-or-name",)
"John Smith V." → family="V." · boundary
"John J. V" → family="V" · boundary
"John Smith XI" → family="XI" · boundary
Accepted: a lower-case `v` in a name written wholly in lower
case is read as the suffix word the vocabulary lists, words to
spare or not, and reports nothing, where the same letter in
capitals is counted and reported; `i`, which is also a
connective, is read as an initial there (P3) and counted.
"john v" → family=""
"john smith v" → ambiguities=()
"john i" → family="i"
"john i" → ambiguities=("conjunction-or-initial",)
Accepted: inside a maiden marker's clause the connective is not
read (M2), so a one-case `i` there is the suffix word the
vocabulary lists, as `v` is.
"john nee smith i" → suffix="i"
Accepted: a numeral with another suffix word behind it is not
the last word of the name and is read as any word there is, so a
one-letter numeral before a credential is the family name.
"John Smith V PhD" → family="V"
history: decisions/S10 · interacts: S2, S4, S7, C1, P2, P3, M2, H1, H3 · implemented: nameparser/_pipeline/_assign.py, nameparser/_pipeline/_group.py, nameparser/_pipeline/_pieces.py, nameparser/_pipeline/_vocab.py
Loading
Loading