Skip to content

Commit 3b2b6f0

Browse files
derek73claude
andauthored
Stop the chain before a trailing title after a family comma (#623) (#635)
Assign's walk over a family comma's given part already reads the trailing title run (H5), but group bounded the particle chain there by `trailing_start`, the suffix peel alone, so the chain took the title into the particle's name first: `Smith, John van der Berg Prof.` read middle 'van der Berg Prof.'. At that site the bound is now `_pieces.trailing_run_start`, the run `tail_reading` takes with its titles, so the title is the title; P6 reads the given part as though the title were absent (`Smith, Jan van Prof.` -> family 'van Smith'). The bound is asked only where a particle stands past the chain's lead, which also drops a call master made on every family-comma part. Decided (Derek): this narrower bound rather than the issue's proposed second read of the given part; and where the bound and the walk disagree on a word behind a title (a numeral, a particle, a period- marked title-particle), recorded rather than narrowed -- garbage input. Measured against 4d5631c: 17,340 of 73,920 given-part grid parses move, all but 500 the title into the title field; H5 transparency broken on 392 texts against master's 3,048, none newly broken. Five gates exit 0 with a fix(#623) rule. Reference frames unchanged. Reviews added rows pinning the particle gate, the empty head and the chain's re-ask (live, which closed #638), and a frame test for the gate. Follow-ups: #636, #637. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 4d5631c commit 3b2b6f0

15 files changed

Lines changed: 239 additions & 28 deletions

‎docs/design/decisions.md‎

Lines changed: 7 additions & 2 deletions
Large diffs are not rendered by default.

‎docs/design/rules.md‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -333,14 +333,19 @@ H5. Rationale: a word abbreviated with a period at the END of a name
333333
particle chain is made, so a particle chain (P2) stops in front
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
336-
word for the chain to take. After a family comma the given part
337-
is read once all its words are joined, so there a particle chain
338-
takes a trailing title as before. A maiden clause ends where the run the end of the name
336+
word for the chain to take. After a family comma the given part's
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
339+
title is the title and P6's attachment reads the particle in
340+
front of it as though the title were absent. A maiden clause ends where the run the end of the name
339341
reads without it begins (M2), so where a trailing rule reads the
340342
part a trailing title ends the clause and is a title.
341343
"John van der Berg Prof." → title="Prof."
342344
"John van der Berg Prof." → family="van der Berg"
343345
"Mary Smith née Jones Prof." → title="Prof."
346+
"Smith, John van der Berg Prof." → title="Prof."
347+
"Smith, John van der Berg Prof." → middle="van der Berg"
348+
"Smith, Jan van Prof." → family="van Smith"
344349
Accepted: what the chain leaves is also what counts as a name
345350
word to spare (P5). A trailing title word is not one, so a bound
346351
given-name word behind one joins exactly as it joins with the
@@ -354,7 +359,7 @@ H5. Rationale: a word abbreviated with a period at the END of a name
354359
gap in the segment path rather than a boundary of this rule, and
355360
tracked as part of #316. Pinned by a case row rather than by an
356361
example here, so recording the gap does not make it normative.
357-
history: decisions.md#H5 · interacts: H1, H2, H3, H4, M2, P2, P5, S2, C1 · implemented: nameparser/_pipeline/_assign.py, nameparser/_pipeline/_pieces.py
362+
history: decisions.md#H5 · interacts: H1, H2, H3, H4, M2, P2, P5, P6, S2, C1 · implemented: nameparser/_pipeline/_assign.py, nameparser/_pipeline/_group.py, nameparser/_pipeline/_pieces.py
358363

359364
## Particles & surname prefixes (P)
360365

@@ -976,7 +981,7 @@ P6. Rationale: a particle ending the name has nothing to link
976981
negative-control sweep pinning the disagreeing set the precedence
977982
bullet above names. A change that breaks one side of that pair
978983
should expect that test, not this file, to say so first.
979-
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
980985

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

‎docs/release_log.rst‎

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

2323
- **Fix a credential run after a comma losing the name in front of it when two surname particles stand side by side in the run.** ``HumanName("John Smith, PhD DO DO")`` gives first ``John``, last ``Smith``, suffix ``PhD DO DO``, where 2.2 and 2.3 gave first ``PhD``, last ``DO DO John Smith`` and 2.0 and 2.1 gave title ``PhD``, first ``DO DO`` -- 1.4.0's reading, restored. ``DO``, ``MC`` and ``VD`` are credentials and surname particles at once, and two particles next to each other had joined into one particle run that took the credentials apart; the part after the comma is now read as credentials before any particle can join it. ``John Smith, PhD vd DO`` gives suffix ``PhD vd DO`` the same way, and ``John Smith, MD DO DO`` gives suffix ``MD DO DO`` where 2.3 gave title ``MD``, first ``DO``, middle ``DO``. A single ``DO`` is still left to its capitals (``John Smith, PhD DO`` gives suffix ``PhD DO``), and with one name word before the comma a part of credentials is all suffixes (``Smith, PhD DO DO`` gives last ``Smith``, suffix ``PhD DO DO``, and ``Smith, MA DO DO`` last ``Smith``, suffix ``MA DO DO``, where 2.3 gave first ``PhD`` and first ``MA``, last ``DO DO Smith``). See the #562 entry under ``C1`` in ``docs/design/decisions.md`` (closes #562)
2424

25-
- **Fix a trailing title being taken into a surname that begins with a particle.** ``HumanName("John van der Berg Prof.")`` gives title ``Prof.``, last ``van der Berg``, where every release from 1.4.0 through 2.3.0 gave last ``van der Berg Prof.``. The suffixes and titles at the end of a name are now read before the surname's particles join the words after them, so the particles no longer take a title in. Behind a family comma the given part is read as before (``HumanName("Smith, John van der Berg Prof.")`` keeps middle ``van der Berg Prof.``). The same reading order brings back 1.4.0's ``HumanName("abdul J. V")``: first ``abdul J.``, last ``V``, where 2.2 and 2.3 gave first ``abdul``, middle ``J.``. See the #614 entry under ``S2`` in ``docs/design/decisions.md`` (closes #614)
25+
- **Fix a trailing title being taken into a surname that begins with a particle.** ``HumanName("John van der Berg Prof.")`` gives title ``Prof.``, last ``van der Berg``, where every release from 1.4.0 through 2.3.0 gave last ``van der Berg Prof.``. The suffixes and titles at the end of a name are now read before the surname's particles join the words after them, so the particles no longer take a title in. The given part behind a family comma reads the same way: ``HumanName("Smith, John van der Berg Prof.")`` gives title ``Prof.``, middle ``van der Berg``, and ``Smith, Jan van Prof.`` gives title ``Prof.``, last ``van Smith``, as ``Smith, Jan van`` gives last ``van Smith``, where 1.4.0 through 2.3.0 gave middle ``van der Berg Prof.`` and ``van Prof.``. The same reading order brings back 1.4.0's ``HumanName("abdul J. V")``: first ``abdul J.``, last ``V``, where 2.2 and 2.3 gave first ``abdul``, middle ``J.``. See the #614 entry under ``S2`` and the #623 entry under ``H5`` in ``docs/design/decisions.md`` (closes #614, #623)
2626

2727
- **New Policy field unlisted_dotted_suffixes, on by default: a dotted acronym nobody has listed is read by position.** ``HumanName("John Smith X.Y.Z.")`` gives suffix ``X.Y.Z.`` where every release gave last ``X.Y.Z.``, while ``Jack X.Y.Z.`` keeps its surname, the same words-to-spare rule a listed acronym takes -- and both readings are reported. After a comma the count is of the words before it, and two dotted single letters are the exception: they are how a person's initials are written, and two words before a comma may be one surname, so ``García Márquez, G.J.`` keeps first ``G.J.`` and last ``García Márquez`` and reports the fork, unless an unambiguous post-nominal in front of the initials that is not also a title, or another unlisted dotted word beside them, says otherwise (``John Smith, PhD X.Y.`` gives suffix ``PhD X.Y.``, while ``García Márquez, Ms G.J.`` keeps title ``Ms``, first ``G.J.``). Three letters or more read by the count, so ``John Smith, X.Y.Z.`` gives suffix ``X.Y.Z.`` -- and so does ``García Márquez, G.J.R.``, the accepted cost of the line, since initials are conventionally written apart (``García Márquez, G. J. R.``), as separate words this rule does not read (#563). Case is irrelevant here: the periods are the signal, so ``john smith x.y.z.`` reads the same way. Words the vocabulary does know are untouched (``M.A.``, ``Ph.D.``, ``A.B.C.``), a single trailing period is still not this shape (``John Smith Xyz.`` keeps last ``Xyz.``), and a dotted run at the FRONT of a name is untouched (``J.R.R. Tolkien``). One accident retires with it: a dotted word whose only vocabulary matches were SINGLE ASCII CHARACTERS -- the roman numerals the suffix list holds, and the lone digit ``2`` -- was reading as a generational suffix, so ``Jack X.Y.I.`` gives last ``X.Y.I.`` again, as 1.4.0 read it, while ``Msc.Ed.``, ``JD.CPA`` and ``Lt.Gov.`` are unchanged. The digit is why a dotted VERSION STRING moves with them and moves SILENTLY: ``John Smith 1.4.2`` gives last ``1.4.2`` where 2.3 gave suffix ``1.4.2``, and ``John Smith, 1.4.2`` gives first ``1.4.2``, last ``John Smith``. Such a token reports nothing at any policy -- it is no acronym either, the shape reading wanting every chunk alphabetic -- and a version string read as a credential was the same accident this retirement removes. That retirement is NOT behind this switch and stands either way -- setting it to ``False`` reads an unlisted dotted word as name material by position instead (``John Smith X.Y.Z.`` keeps last ``X.Y.Z.``), the pre-2.4 reading for THAT half alone. See the ``S2`` and ``suffix-acronym-collisions`` entries of ``docs/design/decisions.md`` (closes #516)
2828

‎nameparser/_pipeline/_group.py‎

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@
6666
Peel, TailRead, chain_lead, chain_run_end, is_conj_piece, is_leading_title, is_prefix_piece,
6767
is_suffix_piece, is_title_piece, join_connectives, joined_tags,
6868
leading_titles, merge_particle_runs, merge_pieces, peel_walk,
69-
read_trailing_run, tail_reading, trailing_candidates, trailing_start,
69+
read_trailing_run, tail_reading, trailing_candidates,
70+
trailing_run_start, trailing_start,
7071
trailing_start_past_titles,
7172
)
7273
from nameparser._pipeline._state import (
@@ -805,8 +806,10 @@ def _group_segment(seg: tuple[int, ...], additional: int,
805806
# suffix" -- on a main segment the run read above decides that
806807
# and is split off already; elsewhere group does not decide
807808
# it, and stops before whatever
808-
# trailing_start says the run is, so the chain ends where assign's
809-
# 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
810813
# `tail_reading` over the clause-free view instead (#601), so the
811814
# clause ends where assign's peel and title chain together begin.
812815
# rules.md#P2: "a particle joins the words after it into one name
@@ -903,10 +906,32 @@ def _group_segment(seg: tuple[int, ...], additional: int,
903906
# asks again after its merges whether the acronym still has
904907
# the pieces the fork counted (below).
905908
# the run read above is already split off (#614), so the chain
906-
# stops at the end of what is left
907-
tail = (0 if read is not None
908-
else len(pieces) - trailing_start(name_start, pieces, ptags,
909-
tokens, one_case=one_case))
909+
# stops at the end of what is left.
910+
# rules.md#H5: "the title is TRANSPARENT to the suffix reading"
911+
# -- in the given part after a family comma too, where assign's
912+
# walk reads the trailing title run, so the chain stops before
913+
# the run of suffixes and titles `tail_reading` takes there
914+
# rather than before the suffixes alone, and does not carry a
915+
# trailing title into the particle's name ('Smith, John van der
916+
# Berg Prof.' reads title 'Prof.' as 'Smith, John Prof.' does,
917+
# #623). The bound matters only where a particle stands past the
918+
# chain's lead, so it is not asked otherwise: the chain then
919+
# merges nothing (the test is `is_prefix_piece`'s, inline).
920+
chains = False
921+
if read is None:
922+
for k in range(leading + 1, len(pieces)):
923+
if ("prefix" in ptags[k]
924+
or len(pieces[k]) == 1
925+
and "particle" in tokens[pieces[k][0]].tags):
926+
chains = True
927+
break
928+
given_slot = site is ClauseSite.GIVEN_SLOT
929+
tail = (0 if not chains
930+
else len(pieces) - (
931+
trailing_run_start(name_start, pieces, ptags, tokens,
932+
one_case) if given_slot
933+
else trailing_start(name_start, pieces, ptags, tokens,
934+
one_case=one_case)))
910935
# the members the chain reports below, which the read's picks
911936
# then give up (#614)
912937
took: set[tuple[int, ...]] = set()
@@ -1018,7 +1043,9 @@ def chain(tail: int) -> None:
10181043
#
10191044
# The DECLINED reading is what this reports, because
10201045
# this branch only runs over pieces the peel left
1021-
# 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
10221049
# scan stops at `len(pieces) - tail`, and a piece the
10231050
# peel TOOK is behind that bound. Where the chain runs
10241051
# again with a smaller tail (the re-peel below), the
@@ -1077,10 +1104,12 @@ def chain(tail: int) -> None:
10771104
k += 1
10781105

10791106
# Where the run was read above (#614) it is split off and
1080-
# `tail` is 0, so none of what follows runs; it is the path for
1081-
# the segments that read no run above -- a family comma's
1107+
# `tail` is 0, so none of what follows runs, as it does not
1108+
# where no particle stands past the lead (#623); it is the path
1109+
# for the segments that read no run above -- a family comma's
10821110
# parts, a part past the second comma, a run with a name piece
1083-
# inside it.
1111+
# inside it -- and the given part reaches it still
1112+
# ('Smith, Freiherr van Berg X.Y.Z.', the row that holds it).
10841113
#
10851114
# The peel was read over the pieces as they stand, and the
10861115
# chain's own merges can change what it counts: behind a word
@@ -1102,9 +1131,12 @@ def chain(tail: int) -> None:
11021131
kept = ([list(q) for q in pieces], [set(t) for t in ptags],
11031132
len(ambiguities))
11041133
chain(tail)
1105-
left = len(pieces) - trailing_start(
1106-
leading_titles(pieces, ptags, tokens), pieces, ptags,
1107-
tokens, one_case=one_case)
1134+
start = leading_titles(pieces, ptags, tokens)
1135+
left = len(pieces) - (
1136+
trailing_run_start(start, pieces, ptags, tokens, one_case)
1137+
if given_slot
1138+
else trailing_start(start, pieces, ptags, tokens,
1139+
one_case=one_case))
11081140
if left < tail:
11091141
pieces[:], ptags[:] = kept[0], kept[1]
11101142
del ambiguities[kept[2]:]

‎nameparser/_pipeline/_pieces.py‎

Lines changed: 24 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
@@ -1896,6 +1898,27 @@ def read_trailing_run(pieces: Sequence[Sequence[int]],
18961898
frozenset(run_titles), peel), start
18971899

18981900

1901+
def trailing_run_start(start: int, pieces: Sequence[Sequence[int]],
1902+
ptags: Sequence[Set[str]],
1903+
tokens: Sequence[WorkToken],
1904+
one_case: bool | None) -> int:
1905+
"""Where the run `tail_reading` takes over `pieces[start:]` begins,
1906+
its trailing titles included: the first index of a piece the peel
1907+
or the H5 chain took, `len(pieces)` where they take none. The
1908+
bound for a reading that takes the trailing titles as well as the
1909+
suffixes, as assign's walk does in the given part after a family
1910+
comma (#623), where `trailing_start` sees only the suffixes and
1911+
`trailing_start_past_titles` leaves the titles inside the bound."""
1912+
rest, titled, peel = tail_reading(peel_walk(start, ptags), pieces,
1913+
ptags, tokens, one_case)
1914+
first = len(pieces)
1915+
for k in rest[peel.names:]:
1916+
first = min(first, k)
1917+
for k in titled:
1918+
first = min(first, k)
1919+
return first
1920+
1921+
18991922
# rules.md#H5: "the title is TRANSPARENT to the suffix reading: where
19001923
# two or more name words stand, what stands once the chain is taken
19011924
# reads exactly as it would read written without the title, plus the

0 commit comments

Comments
 (0)