Skip to content

Commit 4455962

Browse files
derek73claude
andauthored
Make no particle chain in a part that holds no name word (#636) (#639)
A part past the second comma, after either comma, reads wholly as suffixes with its title words as titles (C2), but group still ran the particle chain over it, building a name unit no reading uses: `John Smith, Jr., van der Berg Prof.` read suffix 'Jr., van der Berg Prof.' where `John Smith, Jr., Prof.` reads title 'Prof.'. Group now makes no chain there and asks no bound for one, so C2 reads every title word in the part, bare or mid-part. Decided (Derek): this, rather than #623's trailing-title bound the issue proposed. The part after a suffix comma had reached no chain since #613; the tail half of group's report handoff, dead with no chain there, is removed. Measured against 3b2b6f0: 18,318 parses on 6,106 texts of #624's comma-tail grid move, 15,330 a title word into the title field and 2,988 a particle back out of a title the chain had joined; no corpus name, case text or other grid moves. Five gates exit 0. Tail parts cost fewer frames. Rows pin the suffix-comma, family-comma and empty-head tails. Follow-ups: #640, #641. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 3b2b6f0 commit 4455962

12 files changed

Lines changed: 130 additions & 36 deletions

File tree

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎docs/design/decisions.md‎

Lines changed: 5 additions & 1 deletion
Large diffs are not rendered by default.

‎docs/design/rules.md‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2080,15 +2080,18 @@ C2. Rationale: text beyond the recognized comma parts should be
20802080
about reading a word of it as a name, whether it is the part
20812081
after a suffix comma (C1) or a part beyond the second: nothing
20822082
in it is read as one, a maiden marker there being an ordinary
2083-
word (M2), so a particle chain run over it (P2) reports neither a
2084-
particle chained onto a name word nor an acronym taken into the
2085-
name. What such a part reports is its own — C1's flip and this
2086-
rule's flag.
2083+
word (M2), and no particle chain (P2) is made in it, so a title
2084+
word behind a particle run there is a title like any other, and
2085+
the part reports neither a particle chained onto a name word nor
2086+
an acronym taken into the name. What such a part reports is its
2087+
own — C1's flip and this rule's flag.
20872088
"John Smith, MD, Bart" → suffix="MD, Bart"
20882089
"Eric H. Holder, Jr., Attorney General" → title="Attorney General"
20892090
"Eric H. Holder, Jr., Attorney General" → ambiguities=()
20902091
"Eric H. Holder, Jr., Secretary of State" → ambiguities=("comma-structure",) · boundary
20912092
"Smith, John, Prof." → title="Prof."
2093+
"John Smith, Jr., van der Berg Prof." → title="Prof."
2094+
"John Smith, Jr., van der Berg Prof." → suffix="Jr., van der Berg"
20922095
"John Smith, Jr., Lt.Gov." → ambiguities=()
20932096
"John Smith, MD, Ms" → suffix="MD, Ms" · boundary
20942097
"John Smith, MD,, Jr." → suffix="MD, Jr." · boundary
@@ -2099,10 +2102,12 @@ C2. Rationale: text beyond the recognized comma parts should be
20992102
"Steven Hardman, MD, DO, DDS" → ambiguities=()
21002103
"STEVEN HARDMAN, MD, DO, DDS" → ambiguities=("comma-structure",) · boundary
21012104
"John Smith, MD, XYZ" unlisted_caps_suffixes-everywhere → ambiguities=("comma-structure",)
2102-
Accepted: the no-name-reading clause carries no example line of
2103-
its own. What it moves is a report with no field beside it, and
2104-
an example line would enter the rules corpus for that report
2105-
alone; its executable witnesses are the case rows
2105+
Accepted: the no-name-reading clause's REPORT half carries no
2106+
example line of its own. What it moves is a report with no field
2107+
beside it, and an example line would enter the rules corpus for
2108+
that report alone; its title half is witnessed by the two `John
2109+
Smith, Jr., van der Berg Prof.` lines above, and its report half's
2110+
executable witnesses are the case rows
21062111
a_credential_run_after_the_comma_reports_no_chain_fork and
21072112
a_part_past_the_second_reports_no_particle_fork in
21082113
tests/v2/cases.py, read beside the two rows the chain still

‎docs/release_log.rst‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Release Log
4444

4545
- **Fix a comma part read as credentials being taken apart again by a later join.** ``HumanName("John Smith, Ph. D. and Mary Jones")`` gives first ``John``, last ``Smith``, suffix ``Ph. D. and Mary Jones``, where 2.3.0 gave first ``Ph. D. and Mary``, middle ``Jones``, last ``John Smith``, and ``parse()`` reports each name word the part took: the parser now decides once, after every word is recognized, whether the part after a comma holds the credentials, and nothing joins across that decision afterwards. Titles joined by ``and`` stay one title (``John Smith, Mr. and Mrs.`` keeps title ``Mr. and Mrs.``). A surname joined by ``y`` counts as its words, as #575 already counted it in front of a credential: ``Ortega y Gasset, Dr.`` gives title ``Dr.``, first ``Ortega``, middle ``y``, last ``Gasset``, where 2.3.0 gave last ``Ortega y Gasset``. And a surname with a suffix in it before the comma is the surname, as ``Smith Jr., John`` always read it: ``Smith Jr., Esq.`` gives last ``Smith``, suffix ``Jr., Esq.``, where 2.3.0 gave first ``Smith``, and ``Smith Jr., V`` gives first ``V``, last ``Smith``, suffix ``Jr.``, where 2.3.0 gave first ``Smith``, suffix ``Jr., V``. See the #613 entry under ``C1`` in ``docs/design/decisions.md`` (closes #613)
4646

47-
- **Change a title word in a part after a second comma to read as a title.** ``HumanName("Eric H. Holder, Jr., Attorney General")`` gives title ``Attorney General``, suffix ``Jr.``, where 1.4.0 through 2.3.0 gave suffix ``Jr., Attorney General``, and the part no longer reports ``comma-structure``. A word that is also a suffix stays a suffix (``John Smith, MD, Ms``). A title joined by a connective is read as a title, but its part keeps the report (``Secretary of State``). Since the part's words can land in two fields, the report no longer names one: ``parse("John Smith, Jr., Dr. Bart")`` reports its last part as "beyond the recognized comma structures; read best-effort", where 2.3.0 said "consumed as suffix best-effort". (#603, closes #629)
47+
- **Change a title word in a part after a second comma to read as a title.** ``HumanName("Eric H. Holder, Jr., Attorney General")`` gives title ``Attorney General``, suffix ``Jr.``, where 1.4.0 through 2.3.0 gave suffix ``Jr., Attorney General``, and the part no longer reports ``comma-structure``. A word that is also a suffix stays a suffix (``John Smith, MD, Ms``). A title joined by a connective is read as a title, but its part keeps the report (``Secretary of State``). Since the part's words can land in two fields, the report no longer names one: ``parse("John Smith, Jr., Dr. Bart")`` reports its last part as "beyond the recognized comma structures; read best-effort", where 2.3.0 said "consumed as suffix best-effort". A title word behind a surname particle run there reads as a title too: ``HumanName("John Smith, Jr., van der Berg Prof.")`` gives title ``Prof.``, suffix ``Jr., van der Berg``, where 1.4.0 through 2.3.0 gave suffix ``Jr., van der Berg Prof.``. (#603, closes #629, #636)
4848

4949
- **Add the Catalan and Polish surname link.** ``parse("Josep Carod i Rovira")`` gives family ``Carod i Rovira``, where every release from 1.4.0 through 2.3.0 gave middle ``Carod i`` with family ``Rovira``; ``Josep Lluis Carod i Rovira`` gives middle ``Lluis`` with that same family; and ``Carod i Rovira, Josep`` gives it too, where they read family ``Carod Rovira`` and took the link into ``suffix`` as a generation marker. ``i`` is connective vocabulary now, the way ``y`` already was, and a connective counts as a name word wherever the three-word carve-out counts them -- whatever else the vocabulary says the word is, which matters here because ``i`` is also the roman numeral. A connective that is also generational vocabulary joins only where a name word stands on each side of it, so ``John Quincy Smith i`` keeps suffix ``i``, ``Josep Lluis Carod i III`` keeps suffix ``i III``, and the two-word ``Carod i`` keeps its generation reading. Written wholly in one case the letter reads as an initial and says so: ``JOSEP CAROD I ROVIRA`` and ``josep carod i rovira`` keep the fields they had and gain a ``conjunction-or-initial`` report, which a one-case name gains wherever a bare ``i`` or ``I`` stands among the name's own words -- a letter inside a maiden clause is read by the clause's rules and stays silent, as ``e`` already was -- and in an all-lower name that reading can move a field, each such name now reading as its all-caps twin already did (``parse("john smith i jr")`` gives middle ``smith``, family ``i`` and suffix ``jr`` where it gave family ``smith`` and suffix ``i jr``). Case repair follows the reading: a lower-case ``i`` the parse read as the generation is still title-cased by ``capitalize(force=True)`` (``Carod i`` gives ``Carod I``, as every release did), while one standing among the name words keeps its lower case as ``y`` always has (``Carod i Rovira`` gives ``Carod i Rovira``, where ``Carod I Rovira`` was the pre-2.4 answer). A link inside a maiden clause stays in the birth name, which no release read that way: ``HumanName("Jane Doe nee Puig i Soler")`` gives maiden ``Puig i Soler`` with last ``Doe``, where 2.0 through 2.3 ended the birth name at the link and gave maiden ``Puig`` with middle ``Doe i``, last ``Soler`` -- and the same words would have joined into last ``Doe i Soler`` under the change above, carrying a word of the birth name into the current surname. ``Jane Doe née Kowalska i Nowak`` moves with it, as does the all-lower ``jane doe nee puig i soler``; the ``y`` spelling always read this way and is untouched. The link still has to be joining: ``Jane Doe nee Puig i`` keeps maiden ``Puig`` with suffix ``i``, and ``Jane Doe nee Puig i III`` suffix ``i III``. A caller with Catalan or Polish data removes the entry from ``conjunctions_ambiguous`` and gets the join in the one-case names too; a caller who wants none of this removes ``i`` from ``conjunctions``, which restores every prior FIELD and every prior report, with two readings it does not restore and cannot: a letter the two vocabularies disagree about being an initial reads as one here and as the generation there, and case repair leaves a connective the parse placed among the NAME words in lower case where the off switch title-cases it -- ``parse("Dr. John i Smith").capitalized(force=True)`` keeps ``i`` where the off switch gives ``Dr. John I Smith``, and ``Carod i Rovira`` and ``Josep i Rovira`` are the same shape. Those two are the whole of what the switch does not undo, and ``tests/v2/test_properties.py`` states them as its invariants' only exemptions. A delimiter the caller declares through ``Policy(extra_suffix_delimiters=...)`` parts a trailing suffix part as a comma does (see the suffix-delimiter entry below), so no link joins across one: under ``(" - ",)``, ``Smith, John, PhD - i Soler`` keeps suffix ``PhD, i Soler``, as 2.3.0 read it. The default policy declares no such delimiter. See the ``P3`` and ``M2`` entries of ``docs/design/decisions.md`` (closes #397, closes #538)
5050

‎nameparser/_pipeline/_group.py‎

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -917,8 +917,16 @@ def _group_segment(seg: tuple[int, ...], additional: int,
917917
# #623). The bound matters only where a particle stands past the
918918
# chain's lead, so it is not asked otherwise: the chain then
919919
# merges nothing (the test is `is_prefix_piece`'s, inline).
920+
# rules.md#C2: "a part past the second comma holds no name word"
921+
# -- assign reads such a part wholly as suffixes and its title
922+
# words as titles, so the chain builds no name unit there and is
923+
# not run, and a title word behind a particle run stays a word
924+
# C2 can read ('John Smith, Jr., van der Berg Prof.', #636). The
925+
# part after a suffix comma reaches no chain either: `_comma.
926+
# decide` binds it and empties its segment before any join
927+
# (#613).
920928
chains = False
921-
if read is None:
929+
if read is None and site is not ClauseSite.TAIL:
922930
for k in range(leading + 1, len(pieces)):
923931
if ("prefix" in ptags[k]
924932
or len(pieces[k]) == 1
@@ -1107,8 +1115,9 @@ def chain(tail: int) -> None:
11071115
# `tail` is 0, so none of what follows runs, as it does not
11081116
# where no particle stands past the lead (#623); it is the path
11091117
# for the segments that read no run above -- a family comma's
1110-
# parts, a part past the second comma, a run with a name piece
1111-
# inside it -- and the given part reaches it still
1118+
# two name parts and a run with a name piece inside it (a part past the
1119+
# second comma makes no chain, #636) -- and the given part
1120+
# reaches it still
11121121
# ('Smith, Freiherr van Berg X.Y.Z.', the row that holds it).
11131122
#
11141123
# The peel was read over the pieces as they stand, and the
@@ -1141,7 +1150,7 @@ def chain(tail: int) -> None:
11411150
pieces[:], ptags[:] = kept[0], kept[1]
11421151
del ambiguities[kept[2]:]
11431152
chain(left)
1144-
else:
1153+
elif site is not ClauseSite.TAIL:
11451154
chain(0)
11461155
if read is not None and took:
11471156
# built, not `_replace`d: two frames a call
@@ -1320,16 +1329,12 @@ def group(state: ParseState) -> ParseState:
13201329
bound_join = BoundJoin.STRICT
13211330
# Suppressed after a family comma for the same reason _assign
13221331
# suppresses it there: the family name is already fixed, so
1323-
# there is no fork left to report. Suppressed in a tail segment
1324-
# as well, after either comma: assign reads that segment as
1325-
# suffixes and its title words as titles (#603), never as a
1326-
# name, a marker standing in it being an ordinary word
1327-
# (rules.md#M2, #601), so a
1328-
# chain report there -- a particle
1329-
# chained onto a name piece, or an acronym taken into the name
1330-
# -- names a reading the parse never takes. rules.md#C2: "a part
1331-
# the parse consumes wholly as suffixes raises no report about
1332-
# reading a word of it as a name"
1332+
# there is no fork left to report. A tail segment needs no
1333+
# suppression: group makes no particle chain there (#636) and
1334+
# takes no maiden clause (rules.md#M2, #601), so nothing in group
1335+
# reports in one. rules.md#C2: "a part the parse consumes wholly
1336+
# as suffixes raises no report about reading a word of it as a
1337+
# name"
13331338
tail = tail_start is not None and seg_idx >= tail_start
13341339
# #601: where a maiden clause would stand, off the three facts
13351340
# already in hand here -- which decides whether a marker counts
@@ -1382,7 +1387,7 @@ def group(state: ParseState) -> ParseState:
13821387
for part in parts:
13831388
part_pieces, part_ptags, taken = _group_segment(
13841389
part, additional, tokens, bound_join,
1385-
None if (family_comma or tail) else ambiguities,
1390+
None if family_comma else ambiguities,
13861391
state.lexicon.given_name_titles,
13871392
opens_the_name=(seg_idx == 0 and not family_comma),
13881393
one_case=state.one_case,

‎tests/v2/cases.py‎

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2109,10 +2109,11 @@ def _check_cjk_shape_purity(self) -> None:
21092109
ambiguities=("comma-structure",),
21102110
notes="the row above as a part past the second comma, which "
21112111
"is consumed as suffixes but for its title words (C2): "
2112-
"'von' still chains in group, but it ends a suffix, so "
2113-
"the particle-or-given report that 'von' was chained "
2114-
"onto a name piece named a reading the parse never "
2115-
"makes. Every 2.x release through 2.3.0 carried it; the "
2112+
"'von' ends a suffix, so the particle-or-given report "
2113+
"that 'von' was chained onto a name piece named a "
2114+
"reading the parse never makes, and since #636 group "
2115+
"makes no chain in the part at all. Every 2.x release "
2116+
"through 2.3.0 carried the report; the "
21162117
"comma-structure flag is the part's own report. Since "
21172118
"#603 'Freiherr', a title word, reads as the title it "
21182119
"is there; until then the part read wholly as suffix "
@@ -3030,16 +3031,49 @@ def _check_cjk_shape_purity(self) -> None:
30303031
"and the emitter's `j > k + 1` floor is what both have "
30313032
"to clear",
30323033
shape=1),
3034+
Case("a_title_behind_a_particle_run_past_the_second_comma",
3035+
"John Smith, Jr., van der Berg Prof.",
3036+
{"title": "Prof.", "given": "John", "family": "Smith",
3037+
"suffix": "Jr., van der Berg"},
3038+
ambiguities=("comma-structure",),
3039+
classification="fix(#636)",
3040+
notes="a part past the second comma holds no name word "
3041+
"(rules.md#C2), so group makes no particle chain in it "
3042+
"and the title word behind 'van der' is a title as in "
3043+
"'John Smith, Jr., Prof.'; the chain from 'der' had "
3044+
"taken it into the suffix (1.4.0 and 2.3.0 read suffix "
3045+
"'Jr., van der Berg Prof.')"),
3046+
Case("a_title_behind_a_particle_run_after_a_family_comma_tail",
3047+
"Smith, John, van der Berg Prof.",
3048+
{"title": "Prof.", "given": "John", "family": "Smith",
3049+
"suffix": "van der Berg"},
3050+
ambiguities=("comma-structure",),
3051+
classification="fix(#636)",
3052+
notes="the row above after a FAMILY comma: the part past the "
3053+
"second comma holds no name word there either (rules.md#C2) "
3054+
"and makes no chain (#636's review: a chain kept after a "
3055+
"family comma alone passed every other test); 1.4.0 and "
3056+
"2.3.0 read suffix 'van der Berg Prof.'"),
3057+
Case("a_title_behind_a_particle_run_after_an_empty_head_tail",
3058+
",, van der Berg Prof.",
3059+
{"title": "Prof.", "suffix": "van der Berg"},
3060+
ambiguities=("comma-structure",),
3061+
classification="fix(#636)",
3062+
notes="the empty head (AGENTS.md's comma sweep): the part past "
3063+
"the second comma makes no chain, so the title word is a "
3064+
"title (rules.md#C2, #636); 1.4.0 and 2.3.0 read suffix "
3065+
"'van der Berg Prof.'"),
30333066
Case("a_credential_run_after_the_comma_reports_no_chain_fork",
30343067
"John Smith, PhD Do Ma",
30353068
{"given": "John", "family": "Smith", "suffix": "PhD Do Ma"},
30363069
ambiguities=("suffix-or-name",),
30373070
notes="the row above behind a suffix comma: the part after it "
30383071
"is the credential run C1 flips to, and assign reads a "
30393072
"tail segment wholly as suffixes, so 'Ma' ends a suffix. "
3040-
"The chain still runs there ('Do' takes 'Ma') but may "
3041-
"not report taking it into the name, a reading the "
3042-
"parse never makes (rules.md#C2); the one report is "
3073+
"The chain may not report taking it into the name, a "
3074+
"reading the parse never makes (rules.md#C2), and since "
3075+
"#613 it is not run in the part at all, decide binding the "
3076+
"part before any join; the one report is "
30433077
"C1's flip over the whole part. Carried a second, "
30443078
"contradicting suffix-or-name on 'Ma' until 2026-09-28. "
30453079
"1.4.0 read the same fields; 2.3.0 read given 'PhD', "

0 commit comments

Comments
 (0)