Skip to content

Commit fe062ad

Browse files
derek73claude
andcommitted
Fix #623's review: pin the particle gate, record the bound's limits
The review found the bound's particle gate held by no test, two of its narrowings undoing readings master gives (a scan from the name's start rather than the chain's lead, and a test of single-token particles alone): rows `Smith, Freiherr von Berg Prof.` and `Smith, John von und zu Berg Prof.` now hold them, an empty-head row (`, Jan van Prof.`) the comma sweep asks for, and a frame test the gate's saving. Each mutation fails a test. Recorded rather than fixed: where `tail_reading`'s run and the walk disagree on a word behind a title (a numeral, a particle, a period- marked title-particle) the chain stops for a title the walk does not take; every such shape is garbage. Corrected the entry's causes (352 H1, 40 P5), its units and its frame claim; rules.md#H5 no longer says the bound is the walk's run; P6's interacts gains H5; three stale comments name the new bound. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent e76b71e commit fe062ad

12 files changed

Lines changed: 71 additions & 15 deletions

File tree

‎docs/design/decisions.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,8 @@ Decided 2026-09-08 (was Open: [#316](https://github.com/derek73/python-nameparse
609609
- 2026-10-10 (Derek), #623 — AFTER A FAMILY COMMA THE PARTICLE CHAIN STOPS IN FRONT OF A TRAILING TITLE TOO. Assign's walk over the given part already reads the trailing title run, with its own suffix reading (#144's lenient tail with two parts, #602's run, P6's attachment), so `Smith, John Prof.` reads title 'Prof.'. What it lacked was a chain that left the title standing: group bounded the chain there by `trailing_start`, the suffix peel alone, so the chain took the title into the particle's name before the walk met it, and `Smith, John van der Berg Prof.` read middle 'van der Berg Prof.' where `John van der Berg Prof.` reads title 'Prof.' since #614 (1.4.0 and 2.3.0 read middle 'van der Berg Prof.' for the comma spelling). At that site the bound is now `_pieces.trailing_run_start`, where the run `tail_reading` takes begins, its titles included, so the chain stops in front of the title and the walk reads it: title 'Prof.', middle 'van der Berg'. Nothing new is counted -- only the chain's stop moves -- so the issue's question of which words-to-spare floor the given part uses does not arise, and the comma's own readings of a numeral or a bare acronym there (`Smith, John van der Berg VI` middle 'van der Berg VI') do not move. P6 (the issue's open question) reads after the title run, as though the title were absent: `Smith, Jan van Prof.` → title 'Prof.', family 'van Smith', as `Smith, Jan van` reads. The bound is asked only where a particle stands past the chain's lead, by an inline tag scan, since the chain otherwise merges nothing -- which also drops the `trailing_start` call master made for every family-comma part.
610610
DECIDED (Derek): the narrower bound.
611611
Declined: the issue's proposal, reading the given part's run once ahead of the chain and splitting it off as the main segment's is (#614). It would have assign place the given part off group's read, a second reader beside the walk's own predicates, for a class the bound alone closes: the transparency measurement below finds no text the narrower fix leaves broken that the read would mend.
612-
MEASURED 2026-10-10 against 4d5631c2 (#634's merge), py3.11, `nameparser.__file__` asserted on each side. A given-part grid -- `{family}, {head} {middle} {tail}`, and the same with `, Jr.` appended, for family in `Smith`, `van der Berg`, `Ortega y Gasset`, (empty), `(Bob)`; head in `John`, `Dr. John`, `Freiherr`, `abdul`, `Freiherr St`, `J.`; middle in (none), `van`, `van der`, `de la`, `van Berg`, `van der Berg`, `Mc Mc`, `von und zu`, `Berg`, `Berg van`, `do`, `St`, `y Lopez`; tail in (none), `Prof.`, `Dr.`, `Prof. Dr.`, `Prof`, `Sir`, `Prof. PhD`, `PhD Prof.`, `Jr. Prof.`, `Prof. Jr.`, `MA`, `Ma`, `VI`, `V`, `X.Y.Z.`, `Ph. D.`, `Esq.`, `née Jones`, `Prof. Jones`, `Xyz.`, `King.`, `Prof. V`, `MA Prof.`, `Lt.Gov.` -- 18,480 texts under the three orders and `lenient_comma_suffixes=False`, comparing the seven fields and every report: 17,340 role moves, no report-only move. All but 500 put a trailing period-marked title into the title field. H5's transparency over the grid's default-order texts whose given part ends in `Prof.`, `Dr.`, `King.` or `Prof. Dr.` (5,390; the parse with the title against the parse without it, plus the title): broken on 392, against 3,048 on master, and on no text master kept. Of the 392, 368 have an empty or nickname-only head (H1's move behind a title), and 24 a bound given name (`Smith, abdul Dr.` reads given 'abdul Dr.', P5's lenient post-comma join taking the title -- the gap rules.md#H5's Accepted P5 paragraph records). The 500 put a title in front of a numeral with a third comma part (`(Bob), Freiherr Mc Mc Prof. V, Jr.`): the bound reads 'V' as a numeral and stops before 'Prof.', where the walk keeps 'V' a name word with a third part (#144), so 'Prof.' lands in the middle 'Prof. V' rather than the given 'Mc Mc Prof.' -- a name word either way, garbage. The S2 stress grid's comma half (#614's recipe): 56 moves, all a trailing title behind a particle after a family comma (`Smith, Jan Freiherr von Berg Prof.`); #624's title-particle grid: 24, the same class (`Smith, John Freiherr Dr.` → title 'Dr.', family 'Freiherr Smith'). #625's particle grid, every corpus name and case text, and #624's comma-tail grid: 0. The five gates exit 0 with the new `fix(#623)` rule in every ledger. Frames: the reference rows do not move; a family-comma name with a particle pays 2 (`Smith, John van der Berg` 282 → 284 on 3.11, warm) and one without pays less (`Smith, John Quincy` 221 → 213, `Doe, John MA` 254 → 227). Recorded negative control: with the old bound, 8 tests fail -- the two `fix(#623)` rows on both surfaces, the three new rules.md#H5 examples and the field-sweep count.
612+
MEASURED 2026-10-10 against 4d5631c2 (#634's merge), py3.11, `nameparser.__file__` asserted on each side. A given-part grid -- `{family}, {head} {middle} {tail}`, and the same with `, Jr.` appended, for family in `Smith`, `van der Berg`, `Ortega y Gasset`, (empty), `(Bob)`; head in `John`, `Dr. John`, `Freiherr`, `abdul`, `Freiherr St`, `J.`; middle in (none), `van`, `van der`, `de la`, `van Berg`, `van der Berg`, `Mc Mc`, `von und zu`, `Berg`, `Berg van`, `do`, `St`, `y Lopez`; tail in (none), `Prof.`, `Dr.`, `Prof. Dr.`, `Prof`, `Sir`, `Prof. PhD`, `PhD Prof.`, `Jr. Prof.`, `Prof. Jr.`, `MA`, `Ma`, `VI`, `V`, `X.Y.Z.`, `Ph. D.`, `Esq.`, `née Jones`, `Prof. Jones`, `Xyz.`, `King.`, `Prof. V`, `MA Prof.`, `Lt.Gov.` -- 18,480 texts under the three orders and `lenient_comma_suffixes=False`, comparing the seven fields and every report: 17,340 of the 73,920 parses move a role (4,335 texts under each configuration), and no parse moves a report alone. All but 500 parses (125 texts) put a trailing period-marked title into the title field. H5's transparency over the grid's default-order texts whose given part ends in `Prof.`, `Dr.`, `King.` or `Prof. Dr.` (5,390; the parse with the title against the parse without it, plus the title): broken on 392, against 3,048 on master, and on no text master kept. By cause, 352 of the 392 are H1's move of the name behind a title into an empty family (an empty or nickname-only head), and 40 are P5's lenient post-comma join taking the title into a bound given name -- 24 after a named family (`Smith, abdul Dr.` reads given 'abdul Dr.') and 16 behind an empty head (`, abdul Prof.`), the gap rules.md#H5's Accepted P5 paragraph records. The 500 put a title in front of a numeral with a third comma part (`(Bob), Freiherr Mc Mc Prof. V, Jr.`): the bound reads 'V' as a numeral and stops before 'Prof.', where the walk keeps 'V' a name word with a third part (#144), so 'Prof.' lands in the middle 'Prof. V' rather than the given 'Mc Mc Prof.' -- a name word either way, garbage. The S2 stress grid's comma half (#614's recipe): 56 moves, all a trailing title behind a particle after a family comma (`Smith, Jan Freiherr von Berg Prof.`); #624's title-particle grid: 24, the same class (`Smith, John Freiherr Dr.` → title 'Dr.', family 'Freiherr Smith'). #625's particle grid, every corpus name and case text, and #624's comma-tail grid: 0. The five gates exit 0 with the new `fix(#623)` rule in every ledger. Frames (3.11, warm): the reference rows do not move; a family-comma name with a particle pays 2 with no trailing run (`Smith, John van der Berg` 282 → 284), 4 with a suffix (`Smith, John van der Berg Jr.`), and the moved shape itself 50 to 73 (`Smith, John van der Berg Prof.` 310 → 360), a rare shape no guard sees; one without a particle pays less (`Smith, John Quincy` 221 → 213, `Doe, John MA` 254 → 227), which `test_a_given_part_with_no_particle_skips_the_chain_bound` holds (the bound skipped, 'Smith, John Prof.' 250 frames with it asked against 207). Recorded negative control: with the old bound, 8 tests fail -- the two `fix(#623)` rows on both surfaces, the three new rules.md#H5 examples and the field-sweep count.
613+
THE REVIEW (2026-10-10). The bound is `tail_reading`'s run, read as the comma-less name reads it, and the walk reads the given part by its own predicates, so where the two disagree on a word behind a title the chain stops for a title the walk does not take, or cannot cross one to reach a particle P6 takes: a title in front of a shape-only numeral (`Smith, John Mc Prof. VI` → suffix 'Mc', middle 'Prof. VI'; master middle 'Mc Prof. VI'; the walk never reads 'VI' as a suffix after a comma), in front of a numeral with a third comma part (the 500 above), in the middle of the given part in front of a particle (`Smith, John van Prof. Mc` → title 'Prof.', middle 'van', family 'Mc Smith'), and a period-marked title-particle ending it, which P6 attaches (`Smith, John van MA St.` → suffix 'MA', family 'St. Smith'). Every such shape is garbage, most read as master already reads the text without the title (`Smith, John Mc VI` suffix 'Mc'), and none is a reading a well-formed name loses -- over about 1.6 million generated rows under every order, the opt-in policies, one-case writing and caller lexicons, one class (32 rows, `Smith, John Ph. D. St. Prof.`) was transparent to its title on master and is not now. Recorded, not fixed (Derek): garbage, and narrowing the bound would add a condition for it. The review also found the bound's particle gate held by no test, two of its narrowings undoing readings master gives -- a scan from the name's start rather than the chain's lead (`Smith, Freiherr von Berg Prof.`) and a test of single-token particles alone (`Smith, John von und zu Berg Prof.`) -- now rows of their own, with an empty-head row (`, Jan van Prof.`, AGENTS.md's comma sweep), and the frame saving now held as above. Each of the three mutations fails a test. The re-ask after the chain (`left < tail`) changes no reading on master or here, over the suite and 226,776 generated parses: its documented case (`Freiherr von Berg Ma`) takes the read-once path since #614. Left for a follow-up, with two gaps outside this change: a part past the second comma reads a trailing title behind a particle into the suffix (`John Smith, Jr., van der Berg Prof.`, where `John Smith, Jr., Prof.` reads title 'Prof.'), and P5's post-comma join taking a title (the 40 above).
613614

614615
### W1 — unspaced CJK division
615616

‎docs/design/rules.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,8 +334,8 @@ H5. Rationale: a word abbreviated with a period at the END of a name
334334
of a trailing title rather than taking it into the family name.
335335
A title word a connective joined into a longer unit is no single
336336
word for the chain to take. After a family comma the given part's
337-
chain stops in front of the trailing run its walk reads, the
338-
titles in it as well as the suffixes, so there too a trailing
337+
chain stops in front of the trailing run of suffixes and titles,
338+
read as the comma-less name reads it, so there too a trailing
339339
title is the title and P6's attachment reads the particle in
340340
front of it as though the title were absent. A maiden clause ends where the run the end of the name
341341
reads without it begins (M2), so where a trailing rule reads the
@@ -981,7 +981,7 @@ P6. Rationale: a particle ending the name has nothing to link
981981
negative-control sweep pinning the disagreeing set the precedence
982982
bullet above names. A change that breaks one side of that pair
983983
should expect that test, not this file, to say so first.
984-
history: decisions.md#P6 · interacts: A1, C1, P1, S2, P5, M2 · implemented: nameparser/_pipeline/_assign.py, nameparser/_pipeline/_pieces.py, nameparser/_pipeline/_post_rules.py
984+
history: decisions.md#P6 · interacts: A1, C1, H5, P1, S2, P5, M2 · implemented: nameparser/_pipeline/_assign.py, nameparser/_pipeline/_pieces.py, nameparser/_pipeline/_post_rules.py
985985

986986
P7. Rationale: a one-letter particle is spelled with the same letter
987987
as an initial, and the period is what tells them apart. An

‎nameparser/_pipeline/_group.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -806,8 +806,10 @@ def _group_segment(seg: tuple[int, ...], additional: int,
806806
# suffix" -- on a main segment the run read above decides that
807807
# and is split off already; elsewhere group does not decide
808808
# it, and stops before whatever
809-
# trailing_start says the run is, so the chain ends where assign's
810-
# peel begins (#424). The maiden take reads the title-aware
809+
# trailing_start says the run is (the given part after a family
810+
# comma, `trailing_run_start`, its trailing titles included:
811+
# #623, below), so the chain ends where assign's peel begins
812+
# (#424). The maiden take reads the title-aware
811813
# `tail_reading` over the clause-free view instead (#601), so the
812814
# clause ends where assign's peel and title chain together begin.
813815
# rules.md#P2: "a particle joins the words after it into one name
@@ -1041,7 +1043,9 @@ def chain(tail: int) -> None:
10411043
#
10421044
# The DECLINED reading is what this reports, because
10431045
# this branch only runs over pieces the peel left
1044-
# standing: `tail` is the peel's own answer, the inner
1046+
# standing: `tail` is the peel's own answer (with the
1047+
# trailing titles in the given part after a family
1048+
# comma, #623), the inner
10451049
# scan stops at `len(pieces) - tail`, and a piece the
10461050
# peel TOOK is behind that bound. Where the chain runs
10471051
# again with a smaller tail (the re-peel below), the

‎nameparser/_pipeline/_pieces.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -953,7 +953,9 @@ def trailing_start(start: int, pieces: Sequence[Sequence[int]],
953953
as they stand from `start`: the index of the first piece the S2
954954
peel takes, or len(pieces) when it takes none (#424). What P2's
955955
chain stops before where S2's run was not read once ahead of it
956-
(#614: a family comma's parts, a run holding a name piece), and
956+
(#614: the part before a family comma, a part past the second
957+
comma, a run holding a name piece; the given part after a family
958+
comma stops at `trailing_run_start` since #623), and
957959
what M2's walk stops before where no trailing rule reads the
958960
clause -- each had asked "is this a
959961
suffix?" with the suffix-piece test, which vetoes a bare 'V' as

‎tests/v2/cases.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5003,6 +5003,36 @@ def _check_cjk_shape_purity(self) -> None:
50035003
"were absent, so 'van' attaches to the family as in "
50045004
"'Smith, Jan van' (rules.md#H5, #P6; #623); 1.4.0 and "
50055005
"2.3.0 read middle 'van Prof.'"),
5006+
Case("a_title_particle_heading_the_given_part_keeps_the_bound",
5007+
"Smith, Freiherr von Berg Prof.",
5008+
{"title": "Freiherr Prof.", "given": "von Berg",
5009+
"family": "Smith"},
5010+
classification="fix(#623)",
5011+
notes="a word in both the title and particle vocabularies "
5012+
"heads the given part and leads the chain (#624), so the "
5013+
"particle behind it is the one the bound is asked for "
5014+
"(#623's review: a scan from the name's start instead of "
5015+
"the lead skipped it and left the chain unbounded); 1.4.0 "
5016+
"and 2.3.0 read given 'von Berg Prof.'"),
5017+
Case("a_joined_particle_phrase_asks_the_bound",
5018+
"Smith, John von und zu Berg Prof.",
5019+
{"title": "Prof.", "given": "John", "middle": "von und zu Berg",
5020+
"family": "Smith"},
5021+
classification="fix(#623)",
5022+
notes="P3 joins 'von und zu' into one piece tagged a particle, "
5023+
"so it asks the bound as a lone particle does (#623's "
5024+
"review: a test of single-token particles alone left the "
5025+
"chain unbounded here); 1.4.0 and 2.3.0 read middle 'von "
5026+
"und zu Berg Prof.'"),
5027+
Case("a_trailing_title_after_an_empty_head_stops_the_chain",
5028+
", Jan van Prof.",
5029+
{"title": "Prof.", "given": "Jan", "family": "van"},
5030+
ambiguities=("particle-or-given",),
5031+
classification="fix(#623)",
5032+
notes="the empty head (AGENTS.md's comma sweep): the bound "
5033+
"holds there too, and P6's attachment, read in post_rules "
5034+
"behind an empty family part, takes 'van' as the family; "
5035+
"1.4.0 and 2.3.0 read middle 'van Prof.'"),
50065036
Case("a_bare_title_word_after_a_comma_stays_in_the_chain",
50075037
"Smith, John van der Berg Prof",
50085038
{"given": "John", "middle": "van der Berg Prof",

‎tests/v2/test_benchmark.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,25 @@ def test_a_comma_part_with_no_free_connective_skips_the_join() -> None:
881881
only="join_connectives") == 1
882882

883883

884+
def test_a_given_part_with_no_particle_skips_the_chain_bound() -> None:
885+
"""Group asks where the particle chain stops only where a particle
886+
stands past the chain's lead, since the chain merges nothing
887+
otherwise: after a family comma the bound is `trailing_run_start`,
888+
a whole `tail_reading` (#623), which every family-comma name with a
889+
trailing word would pay for nothing. Recorded negative control,
890+
#623's review (2026-10-10, py3.11): with the gate removed no output
891+
moves (226,776 parses compared), the bound is entered once on each
892+
name below, and 'Smith, John Prof.' costs 250 frames against
893+
207."""
894+
if sys.getprofile() is not None:
895+
pytest.skip("a profile hook is already installed; this test owns it")
896+
for text in ("Smith, John Prof.", "Smith, John PhD"):
897+
assert _frames_for(text, only="trailing_run_start") == 0, text
898+
# REACHABILITY: a particle in the given part asks it
899+
assert _frames_for("Smith, John van der Berg",
900+
only="trailing_run_start") == 1
901+
902+
884903
def test_a_segment_with_no_free_connective_skips_the_join() -> None:
885904
"""Group's side of the same gate: a segment with no connective left
886905
free to join runs neither P3's rootname count, whose only reader is

‎tests/v2/test_cases.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,8 @@ def test_the_field_sweep_sees_the_claims_it_checks() -> None:
177177
_FIELD_CLAIM.search(a.detail) is not None
178178
for case, order in _SWEPT_ROWS
179179
for a in _swept(case.id, order).ambiguities)
180-
assert claims == 1153, (
181-
f"the field sweep checks {claims} claims, recorded as 1153 on "
180+
assert claims == 1156, (
181+
f"the field sweep checks {claims} claims, recorded as 1156 on "
182182
f"2026-10-10")
183183

184184

‎tools/differential/expected_since_1.4.0.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4853,7 +4853,7 @@ name_regex = "^Freiherr St John Smith$"
48534853
fields = ["family", "given", "title"]
48544854

48554855
[[change]]
4856-
# rules.md#H5: "chain stops in front of the trailing run its walk reads"
4856+
# rules.md#H5: "chain stops in front of the trailing run of suffixes and titles,"
48574857
# (#623, 2026-10-10): after a family comma a trailing title is the
48584858
# title, and P6 attaches the particle in front of it.
48594859
issue = "fix(#623) after a family comma the chain stops before a trailing title"

‎tools/differential/expected_since_2.0.0.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3935,7 +3935,7 @@ name_regex = "^Freiherr St John Smith$"
39353935
fields = ["_ambiguities", "family", "given", "title"]
39363936

39373937
[[change]]
3938-
# rules.md#H5: "chain stops in front of the trailing run its walk reads"
3938+
# rules.md#H5: "chain stops in front of the trailing run of suffixes and titles,"
39393939
# (#623, 2026-10-10): after a family comma a trailing title is the
39403940
# title, and P6 attaches the particle in front of it.
39413941
issue = "fix(#623) after a family comma the chain stops before a trailing title"

‎tools/differential/expected_since_2.1.0.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3880,7 +3880,7 @@ name_regex = "^Freiherr St John Smith$"
38803880
fields = ["_ambiguities", "family", "given", "title"]
38813881

38823882
[[change]]
3883-
# rules.md#H5: "chain stops in front of the trailing run its walk reads"
3883+
# rules.md#H5: "chain stops in front of the trailing run of suffixes and titles,"
38843884
# (#623, 2026-10-10): after a family comma a trailing title is the
38853885
# title, and P6 attaches the particle in front of it.
38863886
issue = "fix(#623) after a family comma the chain stops before a trailing title"

0 commit comments

Comments
 (0)