Skip to content

Commit ba6946e

Browse files
derek73claude
andcommitted
Fix what the PR #615 review found; titles after form 3's comma in usage.rst
- starts_a_credential_run accepts the merged 'suffix' piece only as the bare Ph./D. pair: a connective join keeps the tag on the wider piece, so 'John Smith, Ph. D. and Mary Jones' opened the part (taking 'Mary' in silence) where 'PhD and Mary' does not. - All five ledgers said 1.4.0 read tail titles as titles; it read them as suffixes. The earlier correction never landed: its script asserted late and wrote nothing. - Comments: C2's ground in assign (no name word past the second comma, not H5), classify (not group) tagging 'Lt.Gov.', the one-piece exit in assign's count stated as the decision it is, and dual-draft leftovers in test_pieces, a case note and segment_suffix_reading. - Rows pinning what no test did: the one-piece exit ('Vega y Lopez, PhD Jones'), the first-suffix-word boundary ('Smith, MA PhD Jones'), the accepted 'Smith, Jr. John', and the connective spelling. - usage.rst (Derek): form 3's part after the comma is "Suffix or Title" too, as it already read ('Eric H. Holder, Attorney General'), with a sentence on period-marked trailing titles; shapes.py follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c6462e0 commit ba6946e

13 files changed

Lines changed: 98 additions & 29 deletions

‎docs/usage.rst‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,16 @@ Every piece of each is optional:
4141

4242
1. ``Title Given "Nickname" Middle Middle Family Suffix``
4343
2. ``Family [Suffix], Title Given (Nickname) Middle Middle[,] Suffix [, Suffix or Title]``
44-
3. ``Title Given Middle Family [Suffix], Suffix [, Suffix or Title]``
44+
3. ``Title Given Middle Family [Suffix], Suffix or Title [, Suffix or Title]``
45+
46+
A title written with a period (``Prof.``) is also read at the end of a
47+
name in forms 1 and 3 and at the end of the given name in form 2;
48+
written bare there (``Prof``), it is a name word, because many title
49+
words are also surnames.
4550

4651
The last two differ in what the comma is doing. In form 2 it separates
4752
the family name from the rest, so the family name comes first; in form
48-
3 it only sets off suffixes, and the name before it is still
53+
3 it only sets off suffixes and titles, and the name before it is still
4954
given-then-family:
5055

5156
.. doctest::
@@ -57,11 +62,15 @@ given-then-family:
5762
>>> parse("John Doe, Jr.").family # form 3
5863
'Doe'
5964

60-
A part after a further comma holds more suffixes, and a word there that
61-
is a title rather than a suffix is read as the title (since 2.4):
65+
A part after the comma in form 3, or after a further comma in either
66+
form, holds suffixes and titles, a word there that is a title rather
67+
than a suffix being read as the title (after a further comma, since
68+
2.4):
6269

6370
.. doctest::
6471

72+
>>> parse("Eric H. Holder, Attorney General").title
73+
'Attorney General'
6574
>>> parse("Eric H. Holder, Jr., Attorney General").title
6675
'Attorney General'
6776

‎nameparser/_pipeline/_assign.py‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,8 +1062,9 @@ def reads_as_a_suffix(m: int, titled: frozenset[int]) -> bool:
10621062
_set_roles(tokens, pieces[k], Role.TITLE)
10631063
# v1 walk order: the first non-title piece is ALWAYS the
10641064
# given, before any suffix check -- 'Smith, V. Jones' keeps
1065-
# first='V.'. Two 2.x deviations are the no-name read above
1066-
# now, so the walk here never meets either: a last piece
1065+
# first='V.'. Two 2.x deviations are the segment reading
1066+
# above now (`reading is not None`), so the walk here never
1067+
# meets either: a last piece
10671068
# that is unambiguously suffix-shaped is a suffix, where v1
10681069
# made it the given ('Andrews, M.D.', 'Smith, Dr. Jr.';
10691070
# classified fix(comma-family)), a segment whose only
@@ -1233,8 +1234,12 @@ def reads_as_a_suffix(m: int, titled: frozenset[int]) -> bool:
12331234
# (`_vocab.surname_unit_facts`), over EVERY token, so a suffix
12341235
# word still stops a particle ('van Jr. Berg, Mr.' is two name
12351236
# words); a unit counts when a non-suffix piece holds a token
1236-
# of it. One piece is one name word at most, settled in C
1237-
# before any of that is built ('Smith, Jr.').
1237+
# of it. One piece is one name word at most, and that is a
1238+
# DECISION, not only a saving: a connective join makes one piece
1239+
# of several units ('Vega y Lopez'), which the unit count alone
1240+
# would read positionally, losing the family to the given name
1241+
# behind a part a credential opens ('Vega y Lopez, PhD Jones').
1242+
# Settled in C before any of that is built ('Smith, Jr.').
12381243
positional = False
12391244
if reading is not None and len(fam_pieces) > 1:
12401245
idx = [i for piece in fam_pieces for i in piece]
@@ -1276,9 +1281,10 @@ def reads_as_a_suffix(m: int, titled: frozenset[int]) -> bool:
12761281
tail = 2
12771282
# rules.md#C2: segments past the structure's name segments are
12781283
# consumed as suffixes, except "a word of the title vocabulary there
1279-
# that is not also suffix vocabulary is a title" (#603) -- the slot
1280-
# is behind a comma, which is where H5 lets a title be read from
1281-
# the end of a name ('Eric H. Holder, Jr., Attorney General'). A
1284+
# that is not also suffix vocabulary is a title" (#603) -- a part
1285+
# past the second comma holds no name word, so the surnames H5
1286+
# keeps a bare trailing title word from misreading are not there to
1287+
# protect ('Eric H. Holder, Jr., Attorney General'). A
12821288
# piece holding a suffix word stays the postnominal the slot makes
12831289
# it, a dual alone as after a family comma (C1) and a dual group's
12841290
# connective join took into a title ('PhD - and MD') alike.

‎nameparser/_pipeline/_pieces.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,10 @@ def starts_a_credential_run(piece: Sequence[int], ptags: Set[str],
290290
'Jones'.
291291
"""
292292
if "suffix" in ptags:
293-
return True
293+
# the Ph./D. pair alone: a connective join keeps the tag on the
294+
# wider piece ('Ph. D. and Mary'), which starts nothing, as
295+
# 'PhD and Mary' does not
296+
return len(piece) == 2
294297
if len(piece) != 1 or not is_suffix_piece(piece, ptags, tokens):
295298
return False
296299
tok = tokens[piece[0]]
@@ -560,7 +563,9 @@ class by SHAPE takes the count instead, which is decided at the
560563
A part OPENED by a credential is the postnominal part however it
561564
goes on (#603, rules.md#C1): where an unambiguous suffix word that
562565
starts #602's run (`starts_a_credential_run`) is the first suffix
563-
word of the part, with only titles in front of it, every later
566+
word of the part, with only titles in front of it (a title/suffix
567+
dual among them ends the chance: a credential behind one opens
568+
nothing), every later
564569
word is read as #602's run reads the given part -- a title word as
565570
a title, anything else as a suffix -- so 'John Smith, PhD Jones'
566571
and 'Smith, PhD Jones' read suffix 'PhD Jones'. A word of the

‎nameparser/_pipeline/_segment.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,9 @@ def titles_and_suffixes(seg: tuple[int, ...]) -> bool:
224224
# rules.md#C2: "a word of the title vocabulary there that is not
225225
# also suffix vocabulary is a title" -- at least one title
226226
# word, and every other word a suffix word
227-
# period_joined_vocab for 'Lt.Gov.', whose chunks group tags a
228-
# title and assign then reads as one
227+
# period_joined_vocab for 'Lt.Gov.', one token that classify
228+
# (not yet run) tags a title by this same call, and that assign
229+
# then reads as one
229230
titles = state.lexicon.titles
230231
rest = tuple(i for i in seg
231232
if _normalize(t := state.tokens[i].text) not in titles
@@ -567,8 +568,9 @@ def titles_and_suffixes(seg: tuple[int, ...]) -> bool:
567568
# rules.md#C2: "a non-empty extra part that is not entirely suffix
568569
# words is flagged as a structural ambiguity rather than rejected"
569570
# -- parts[2:] are consumed as suffixes either way, their title
570-
# words as titles (#603), so a tail segment that is neither gets
571-
# the COMMA_STRUCTURE flag, not a structure veto. The lean reaches this reading too:
571+
# words as titles (#603), so a tail segment that is neither all
572+
# suffix words nor title words beside suffix words gets the
573+
# COMMA_STRUCTURE flag, not a structure veto. The lean reaches this reading too:
572574
# a tail of leaning credentials is a credential run, which is the
573575
# one place this design quiets a report rather than adding one.
574576
for seg in groups[2:]:

‎tests/v2/cases.py‎

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6167,6 +6167,47 @@ def _check_cjk_shape_purity(self) -> None:
61676167
"and this read middle 'Jones', suffix 'Ph. D.' (2.3.0 "
61686168
"too). Found by #603, whose opened part asks the same "
61696169
"predicate (AGENTS.md's spelling sweep)"),
6170+
Case("a_split_credential_joined_to_a_name_opens_nothing",
6171+
"John Smith, Ph. D. and Mary Jones",
6172+
{"given": "Ph. D. and Mary", "middle": "Jones",
6173+
"family": "John Smith"},
6174+
notes="the two spellings agree (rules.md#C1, #603): group's "
6175+
"connective join keeps the merged credential's 'suffix' "
6176+
"piece tag on the wider piece, and starts_a_credential_run "
6177+
"accepted any piece so tagged, so this read suffix "
6178+
"'Ph. D. and Mary Jones' with 'Mary' taken in silently "
6179+
"while 'John Smith, PhD and Mary Jones' reads as here; the "
6180+
"PR review found it, and only the bare Ph./D. pair starts "
6181+
"a run now"),
6182+
Case("a_one_piece_name_before_an_opened_part_is_the_family",
6183+
"Vega y Lopez, PhD Jones",
6184+
{"family": "Vega y Lopez", "suffix": "PhD Jones"},
6185+
ambiguities=("suffix-or-name",),
6186+
classification="fix(#603)",
6187+
notes="one piece before the comma is one name word, even where "
6188+
"a connective joined three units into it; without "
6189+
"assign's len(fam_pieces) > 1 exit the unit count reads "
6190+
"it positionally, given 'Vega y Lopez' and no family. "
6191+
"2.3.0 read given 'PhD', middle 'Jones'"),
6192+
Case("only_the_first_suffix_word_can_open_the_comma_part",
6193+
"Smith, MA PhD Jones",
6194+
{"given": "MA", "family": "Smith", "suffix": "PhD Jones"},
6195+
ambiguities=("suffix-or-name", "suffix-or-name"),
6196+
notes="#603's opener is the part's FIRST suffix word with only "
6197+
"titles in front of it (rules.md#C1): 'MA', a member of "
6198+
"the ambiguous class, opens nothing and ends the leading "
6199+
"run, so the 'PhD' behind it opens nothing either and the "
6200+
"part is the walk's. Letting a later credential open it "
6201+
"reads suffix 'MA PhD Jones'"),
6202+
Case("a_misplaced_generation_opens_the_comma_part", "Smith, Jr. John",
6203+
{"family": "Smith", "suffix": "Jr. John"},
6204+
ambiguities=("suffix-or-name",),
6205+
classification="fix(#603)",
6206+
notes="decisions.md#C1's accepted cost: a generational word "
6207+
"starts S2's run, so it opens the part and 'John' is "
6208+
"absorbed, one word before the comma reading the whole "
6209+
"part as suffixes (Derek). 2.3.0 read title 'Jr.', given "
6210+
"'John'"),
61706211
Case("a_part_of_titles_behind_a_credential_was_already_postnominal",
61716212
"Eric H. Holder, Jr. Attorney General",
61726213
{"title": "Attorney General", "given": "Eric", "middle": "H.",
@@ -8767,8 +8808,8 @@ def _check_cjk_shape_purity(self) -> None:
87678808
"credential 'PSM' OPENS, which reads as #602's run reads "
87688809
"whatever stands behind it (rules.md#C1) -- title 'Dr.', "
87698810
"suffix 'PSM I' -- so the reset no longer decides it. A "
8770-
"dual behind one name word opens nothing, and the reset "
8771-
"is what is left deciding this spelling. 1.4.0 read the "
8811+
"dual opens nothing, and the reset is what is left "
8812+
"deciding this spelling. 1.4.0 read the "
87728813
"old spelling suffix 'Dr., I'"),
87738814
Case("family_comma_run_numeral_after_a_split_credential",
87748815
"Smith, Ph. D. I",

‎tests/v2/pipeline/test_pieces.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,7 @@ def test_strict_ends_the_run_at_the_initial_shaped_numeral() -> None:
130130
Lenient continues the credential run through a one-character
131131
suffix word; strict vetoes initial-shaped words, so the run is no
132132
run at all and the segment falls to the walk. Asked behind a
133-
title/suffix dual, which opens no part when nothing says the name
134-
before the comma is whole (#603). Behind a credential that does
133+
title/suffix dual, which opens no part (#603). Behind a credential that does
135134
open it ('PSM'), strict refuses the numeral the suffix reading but
136135
the opened part takes it in all the same, and hands it to the
137136
caller to report.

‎tests/v2/test_properties.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3388,8 +3388,9 @@ def test_a_one_letter_particle_with_its_period_reads_as_any_initial() -> None:
33883388
#: The recorded negative control: how many grid texts disagree with the
33893389
#: veto removed from ONE site, measured 2026-10-04 (classify 412 and
33903390
#: the facade 121 until #603 the same day, whose opened comma part
3391-
#: reads nine 'DO Ó., ...'-shaped texts the same with the veto off or
3392-
#: on, the credential opening the part either way). Every site moves
3391+
#: reads some texts the same with the veto off or on, the credential
3392+
#: opening the part either way: nine of classify's, 'DO Ó., ...'
3393+
#: shapes, and one of the facade's). Every site moves
33933394
#: some, so none is decoration. _render's is measured without the
33943395
#: particle case masks, as for a caller's one-letter particle that has
33953396
#: none: with the shipped 'ó' mask the repair takes the mask before

‎tools/differential/expected_since_1.4.0.toml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,8 @@ opened part's (rules.md#C1, #603), not that rule's.
828828
# suffix vocabulary is a title" (#603, 2026-10-04): a part past the
829829
# second comma reads its title words as titles, and a part of title and
830830
# suffix words is recognized rather than flagged ('Eric H. Holder, Jr.,
831-
# Attorney General'). 1.4.0 read these titles as titles too.
831+
# Attorney General'). 1.4.0, like 2.0 through 2.3, read them as
832+
# suffixes.
832833
issue = "fix(#603) a title word in a part past the second comma is a title"
833834
name_regex = "^(?:1 & 2, 3 4 5, Mr\\.|Andrew Perkins, Jr\\., Col\\. \\x28Ret\\x29|Eric H\\. Holder, Jr\\., Attorney General|Eric H\\. Holder, Jr\\., Secretary of State|John Smith, Jr\\., Lt\\.Gov\\.|John, Smith, Dr\\.|Smith, John, Prof\\.)$"
834835
fields = ["suffix", "title"]

‎tools/differential/expected_since_2.0.0.toml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3994,7 +3994,8 @@ fields = ["_ambiguities", "family", "given", "middle", "suffix", "title"]
39943994
# suffix vocabulary is a title" (#603, 2026-10-04): a part past the
39953995
# second comma reads its title words as titles, and a part of title and
39963996
# suffix words is recognized rather than flagged ('Eric H. Holder, Jr.,
3997-
# Attorney General'). 1.4.0 read these titles as titles too.
3997+
# Attorney General'). 1.4.0, like 2.0 through 2.3, read them as
3998+
# suffixes.
39983999
issue = "fix(#603) a title word in a part past the second comma is a title"
39994000
name_regex = "^(?:1 & 2, 3 4 5, Mr\\.|Andrew Perkins, Jr\\., Col\\. \\x28Ret\\x29|Eric H\\. Holder, Jr\\., Attorney General|Eric H\\. Holder, Jr\\., Secretary of State|John Smith, Jr\\., Lt\\.Gov\\.|Smith, John, Prof\\.)$"
40004001
fields = ["_ambiguities", "suffix", "title"]

‎tools/differential/expected_since_2.1.0.toml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3939,7 +3939,8 @@ fields = ["_ambiguities", "family", "given", "middle", "suffix", "title"]
39393939
# suffix vocabulary is a title" (#603, 2026-10-04): a part past the
39403940
# second comma reads its title words as titles, and a part of title and
39413941
# suffix words is recognized rather than flagged ('Eric H. Holder, Jr.,
3942-
# Attorney General'). 1.4.0 read these titles as titles too.
3942+
# Attorney General'). 1.4.0, like 2.0 through 2.3, read them as
3943+
# suffixes.
39433944
issue = "fix(#603) a title word in a part past the second comma is a title"
39443945
name_regex = "^(?:1 & 2, 3 4 5, Mr\\.|Andrew Perkins, Jr\\., Col\\. \\x28Ret\\x29|Eric H\\. Holder, Jr\\., Attorney General|Eric H\\. Holder, Jr\\., Secretary of State|John Smith, Jr\\., Lt\\.Gov\\.|Smith, John, Prof\\.)$"
39453946
fields = ["_ambiguities", "suffix", "title"]

0 commit comments

Comments
 (0)