From dc0d62b9e6e2b46204f0b780ea399a0bb1545bb4 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sun, 11 Oct 2026 03:36:33 -0700 Subject: [PATCH 1/3] Decline a title word a trailing rule reads, in the bound join (#637, #640) P5's bound given-name join took the word after `abdul` even where a trailing rule would read it as a title, so the joined piece was no title word: `Berg, abdul Prof.` read given 'abdul Prof.' where `Berg, John Prof.` reads title 'Prof.' (#637), and `John Smith, Jr., abdul Dr. Smith` read suffix 'Jr., abdul Dr. Smith' (#640). The join now declines such a word: after a family comma one H5's trailing run takes from the given part, and in a part past the second comma any title word, which C2 reads. A title word standing in the name still joins (`abdul Sir Smith Berg`), as P5 states. Decided (Derek): this narrower clause over declining every title word, which split real joins. Measured against 4455962f: 1,044 of 15,552 bound-given grid parses move, each a title word into the title field; 280 of #623's given-part grid, the same class; among corpus names and case texts only the row that pinned the gap. Five gates exit 0. Co-Authored-By: Claude Opus 5.5 --- docs/design/decisions.md | 5 ++- docs/design/rules.md | 20 ++++++----- docs/release_log.rst | 4 +-- nameparser/_pipeline/_group.py | 23 ++++++++++--- tests/v2/cases.py | 36 ++++++++++++-------- tests/v2/test_ledger_guards.py | 25 ++++++++++++-- tools/differential/corpus_rules.jsonl | 3 ++ tools/differential/expected_since_2.0.0.toml | 16 +++++++++ tools/differential/expected_since_2.1.0.toml | 16 +++++++++ tools/differential/expected_since_2.2.0.toml | 16 +++++++++ tools/differential/expected_since_2.3.0.toml | 16 +++++++++ 11 files changed, 148 insertions(+), 32 deletions(-) diff --git a/docs/design/decisions.md b/docs/design/decisions.md index 7eb9e011..75977fb2 100644 --- a/docs/design/decisions.md +++ b/docs/design/decisions.md @@ -596,7 +596,7 @@ Decided 2026-09-08 (was Open: [#316](https://github.com/derek73/python-nameparse - **Measured population: FIVE corpus names at the fix, SEVENTEEN after the docs commit that follows it and the two review rounds on it.** The five are the four planted no-comma rows in corpus_issues.jsonl (`John Smith Dr.`, `John Smith Mr.`, `John Smith Prof.`, `John Smith Rev.`) plus `Andrew Perkins (Mgr.)`, which the drafting did not expect. Eleven of the other twelve are rules.md examples entering corpus_rules.jsonl, eight of them this rule's own — `Smith Prof.`, `Dr. John Smith Prof.`, `John Smith Prof. Dr.`, `John Smith Prof. Jr.`, `John Smith Jr. Prof.`, `John Prof. MA`, `Smith, John Prof.` and `Mary Jane King.`, the last of those added by the first 2026-09-09 review of that commit — plus three of #H1's, which reach this walk because the run each puts behind the name word is what the walk takes: `Smith Sir.`, and `Sir John Prof.` and `Dr. Smith Sir.` from the second review round the same day. The twelfth is `毛 泽东 Dr.`, which entered corpus_cjk_tolerated.jsonl in the FIRST review round and is the one member of the population in a native script: the walk runs before the positional read, so the pieces the script test sees are all Han and the family-first order stands. It is a corpus name the walk moves like any other, and the reason it took its own ledger rule rather than a seventeenth alternative is a guard about alternations, not a difference in the argument (the ledger comment beside it says which). So the growth is documentation rather than reach, and both numbers come off the same recipe — parse every corpus name twice, once on the tree and once with the walk disabled, and diff — run on the two trees. **The disabling stub is NOT the same on the two trees, corrected 2026-09-09 in review of the /simplify round below.** At the fix (`5568fdc`) `trailing_titles` returned the count the chain TAKES and both call sites were in assign, so a stub returning 0 took nothing and disabled the walk. Since the /simplify round it returns the count it LEAVES STANDING, so 0 now takes EVERYTHING and `John Smith` raises IndexError before any measurement happens; the disabling stub on this tree is `lambda rest, *a: len(rest)`, and it has to be bound over both `_pieces.trailing_titles`, which is what `tail_reading` calls, and the name `_assign` imported for the segment-1 walk, patching either alone leaving the other walk live. Re-run that way the two figures hold: FIVE corpus names at `5568fdc`, SEVENTEEN on this branch (2026-09-09). That fifth is a genuine hit rather than a misfire: rules.md#S1 drops the brackets and reads the content exactly as if written bare, so `(Mgr.)` is a trailing period-marked title word and reads as one; its own test stays green. Recompute by parsing every name in `tools/differential/corpus*.jsonl` on the tree and on the branch's third commit's parent and diffing the seven name fields plus `ambiguities`. Baselines differ per name — `John Smith Dr.` read suffix `Dr.` at 1.4.0, 2.0.0 and 2.1.0 (`dr` left the suffix vocabulary in 2.2, #296) and family `Dr.` at 2.2.0 — so the ledger entries are per-baseline. - **The inline frame-free gates at the two walk sites were REMOVED in review**, and this is the ONE-PREDICATE-PER-QUESTION half of the entry. Each site had a cheap inline test written to match the walk's own first condition; that is a second implementation of the question, and the measurement that justified it did not survive re-running. The band test runs early in a session where the facade sits at 453, so the "one frame of headroom" claim did not reproduce. With the gates gone the walk costs +1 frame on each entry point, inside the plan's target of two and inside `test_facade_cost_stays_within_its_band`. The walk's own cheapness is where the saving lives instead: the abbreviation-shape test is a compiled regex (a C call, no Python frame) and runs BEFORE the vocabulary call, and almost no name ends in a period-marked word, so the ordinary parse pays one match and stops. - **A pre-existing detail mismatch, recorded and NOT fixed.** #H4's join shape reports `title-or-name` with a `detail` that says the unit was "read as a given name by convention", while under the default order H1 retags the unit to the family — so `Dr. John of Prince` reports that text with the unit in `family`. This rule adds a second input with the same mismatch, `John of Prince Prof.`, and fixes neither: the wording predates this bundle, the fork the kind reports is title-versus-name which no field answers either way, and #H4 already records why the detail names no field for the peel shape. -- 2026-09-09 (review round 2) — **A1's transparency holds for the ONE-name-word case too, and the scope written a day earlier was the defect rather than the boundary.** A1 was scoped that morning to inputs where a name word still stands on both sides of the chain, handing the one-word case to #H1 unconditionally: "the title behind that word decides its field as a title in front of it would". #H1 was then reading BOTH ends of the name as one title run, so `Sir John Prof.` read family `John` where `Sir John` reads given `John` — the trailing title changing what the leading one addressed by, which is exactly the non-transparency A1 denies. The clause is now conditioned on there being no run in FRONT of the word: where one stands it addresses and the chained trailing title only joins the title field, so `X Prof.` reads as `X` plus the title for one name word as for two. Where no run stands in front the trailing one still decides, unchanged — `Smith Prof.` reads family `Smith`, `Smith Sir.` given `Smith`. Nothing in the walk moved; the fix is entirely in which run H1 keys, and #H1's 2026-09-09 entry carries it, the measurement and the mutation. The #P5 clause below reads the same way afterwards and gains its second half: a bound given-name word behind a trailing title joins as it joins with the title absent AND lands in the same field. Said of the COMMA-LESS writing, and the review that found the H1 defect measured why the qualifier is needed: after a family comma the reserve reads no peel at all, so the join fires over the trailing title word and takes it into the given name — `Berg, abdul Prof.` reads given `abdul Prof.` where `Berg, John Prof.` reads title `Prof.`. That is PARITY (1.4.0 reads it the same, measured) and a gap in the segment path rather than a boundary of the rule, so it is recorded rather than fixed here: the clause carries the scope, and the case row `title_word_trailing_behind_a_bound_pair_after_a_comma` pins it without making it normative. Tracked as part of #316. +- 2026-09-09 (review round 2) — **A1's transparency holds for the ONE-name-word case too, and the scope written a day earlier was the defect rather than the boundary.** A1 was scoped that morning to inputs where a name word still stands on both sides of the chain, handing the one-word case to #H1 unconditionally: "the title behind that word decides its field as a title in front of it would". #H1 was then reading BOTH ends of the name as one title run, so `Sir John Prof.` read family `John` where `Sir John` reads given `John` — the trailing title changing what the leading one addressed by, which is exactly the non-transparency A1 denies. The clause is now conditioned on there being no run in FRONT of the word: where one stands it addresses and the chained trailing title only joins the title field, so `X Prof.` reads as `X` plus the title for one name word as for two. Where no run stands in front the trailing one still decides, unchanged — `Smith Prof.` reads family `Smith`, `Smith Sir.` given `Smith`. Nothing in the walk moved; the fix is entirely in which run H1 keys, and #H1's 2026-09-09 entry carries it, the measurement and the mutation. The #P5 clause below reads the same way afterwards and gains its second half: a bound given-name word behind a trailing title joins as it joins with the title absent AND lands in the same field. Said of the COMMA-LESS writing, and the review that found the H1 defect measured why the qualifier is needed: after a family comma the reserve reads no peel at all, so the join fires over the trailing title word and takes it into the given name — `Berg, abdul Prof.` reads given `abdul Prof.` where `Berg, John Prof.` reads title `Prof.`. That is PARITY (1.4.0 reads it the same, measured) and a gap in the segment path rather than a boundary of the rule, so it is recorded rather than fixed here: the clause carries the scope, and the case row `title_word_trailing_behind_a_bound_pair_after_a_comma` pins it without making it normative. Tracked as part of #316. (FIXED by #637, decisions.md#P5, 2026-10-11: the join declines the trailing title word, and the row pins the title.) - 2026-09-09 (the /simplify round on the bundle's PR) — **the trailing peel and the title chain are ONE reading run to a FIXED POINT, and A1's "ONE peel runs over what stands" was a truncation of it.** Two contrived readings contradicted clauses this section states, both found by an altitude review reading the code against the rule rather than by a test. (1) THE SUBTRACTION MODEL. #P5's reserve modelled assign's reading as the peel's count minus the chain's take, on each of the two views the join compares. That arithmetic equals assign's answer only while the chain's take does not change what the peel would do, and at #S2's bare-ambiguous reserve it does: `abdul rahman MA` reads given `abdul`, family `rahman`, suffix `MA` — the join declining because peeling the acronym unjoined and not joined is a suffix reading the join would change — while `abdul rahman MA Prof.` read given `abdul rahman`, family `MA`, the subtraction having counted `MA` a name word to spare. The Accepted clause below, "a bound given-name word behind one joins exactly as it joins with the title absent", was false as stated. (2) THE TRUNCATED FIXED POINT. The re-peel ran ONCE, so a second title was read half way: `John Prof. MA` reads title `Prof.`, given `John`, family `MA`, and `John Prof. MA Prof.` read title `Prof.`, given `John`, family `Prof.`, suffix `MA` — the re-peel un-peeled the acronym and re-exposed the FIRST title, which nothing then took. Both measured on the branch tip, 2026-09-09. - **The fix is one function, `_pieces.tail_reading`:** peel, chain, splice the chained pieces out, peel again over what is left, until the chain takes nothing. Assign calls it in place of its inline sequence and all its reports still come from the final peel; the reserve calls it on both views and compares the suffix lists it returns, and both subtractions are deleted. `abdul rahman MA Prof.` now reads given `abdul`, family `rahman`, suffix `MA` under title `Prof.`, and `John Prof. MA Prof.` reads title `Prof. Prof.`, given `John`, family `MA`. One further clause had to be said in the code rather than left to the arithmetic: the join must not take a piece the chain takes (#P5 — "a trailing title word the run takes is no word to spare"), which the subtraction had enforced by accident, by leaving that piece in the tail it compared. Without it `Sir abdul Prof.` joins the title into the given name; with it the reading is title `Sir Prof.`, given `abdul`, as before. `trailing_titles` returns the count it LEAVES STANDING in the same edit, the way `peel_trailing` counts, so the two compose at the call site without arithmetic at all. - **Cost, and what paid it.** The shared helper is one frame on the common path, where the inline sequence was none. It is paid by `_effective_order` taking the segment's pieces and the name-piece indices — the shape every piece-layer predicate takes — instead of a list built for it, which on 3.11 is a comprehension frame on every parse; the reference name stays at 416 parse / 453 facade. The helper returns a bare 3-tuple rather than a NamedTuple for the same budget: a NamedTuple's `__new__` is itself a frame, measured at +1 here. @@ -954,6 +954,9 @@ Excluded (MAIDEN_MARKERS, per nameparser/config/maiden_markers.py): - Polish "z domu" — INCLUDED 2026-08-26 as the set's first phrase entry, and the contrast to draw against the roz removal below rather than against any included word. Its words fail C-i separately and it passes as a pair; the argument is in the #434 entry under #M2. The point to carry away is that the entry below could not have been rescued the same way: roz is a whole marker as written, so there is no longer unit to move the claim onto. - Czech/Slovak "roz." — SHIPPED through 2.1 and removed 2026-08-26 (Derek's call). Roz is an ordinary English diminutive of Rosalind, and matching is whole-token, case-folded and period-insensitive, so "Roz", "roz" and "roz." are one string to this set. The collision is not theoretical and it is not new: on the bare path, which predates the 2.2 work entirely, "Rosalind Roz Smith" read maiden "Smith" and NO family name at all, and "Rosalind Roz Jones Smith" read maiden "Jones Smith", because M2 hands the marker every word after it. Grounded in #vocabulary-collisions C-i — a word belongs in its set's ambiguous subset iff it is borne as an ordinary name IN THE POSITION THE VOCABULARY CLAIM ACTS ON, and under uncertainty default to ambiguous. Roz is borne exactly there: the word after a given name, which is the only position M2's claim acts on. What C-i cannot do for this set is the marking: MAIDEN_MARKERS has no ambiguous subset, and no code path consults one, so the only two expressions of C-i available here are ship and do not ship. That is why the remedy is removal rather than an ambiguous marking, and it is worth knowing before someone proposes marking the next collision instead. The full participle rozená stays and is unambiguous; a Czech caller who needs the abbreviation adds it to their own Lexicon, which is the same answer this section gives for every locale-specific vocabulary. The cost is accepted and stated: "Anna Nováková roz. Svobodová" now reads middle "Nováková roz.", family "Svobodová" — which is how 1.4.0 read it — and "Rosalind Roz Smith" is back to 1.4.0's given/middle/family. Pinned by tests/v2/cases.py::diminutive_that_was_a_marker_keeps_the_family and ::full_participle_marker_still_consumes. +- 2026-10-11 (Derek), #637 and #640 — THE BOUND JOIN DECLINES A TITLE WORD A TRAILING RULE READS AS A TITLE. P5 joins a bound given-name word to the word after it, and declined a maiden marker and a suffix piece; a title word it took, so where a trailing rule would have read that word as a title the joined piece was no title word to read. After a family comma the join reads no peel (the LENIENT reserve, #423), so `Berg, abdul Prof.` read given 'abdul Prof.' where `Berg, John Prof.` reads title 'Prof.' (#637, the gap the 2026-09-09 A1 entry recorded); in a part past the second comma after a suffix comma, where the join runs STRICT, `John Smith, Jr., abdul Dr. Smith` read suffix 'Jr., abdul Dr. Smith' where `John Smith, Jr., Berg Dr. Smith` reads title 'Dr.' (#640). Now the join declines a title word a trailing rule reads as a title: after a family comma, one H5's trailing run takes from the given part (`tail_reading`, asked only when a title word follows the bound word), and in a part past the second comma any title word, which C2 reads as a title. `Berg, abdul Prof.` → title 'Prof.', given 'abdul'; `John Smith, Jr., abdul Dr. Smith` → title 'Dr.'. + DECIDED (Derek): this narrower clause (a′) rather than declining every title word (a). P5 states that a title word standing in the name is a name word and joins like one (H3), and (a) undid it: it failed six tests, a P5 rules example and the two tests named for that clause among them, and over the bound-given grid below moved about 4,000 parses, 1,944 of them a real join split (`(Bob), Abu Bishop` → given 'Abu', middle 'Bishop'). (a′) leaves every join on a title word inside the name standing -- `abdul Sir Smith Berg`, `Berg, abdul Prof. Ali`, `Smith, abdul Bishop`. + MEASURED 2026-10-11 against 4455962f (#639's merge), py3.11, `nameparser.__file__` asserted on each side. A bound-given grid -- `{head} {bound} {next} {rest}` for head in (none), `Dr.`, `Sir`, `Smith,`, `van der Berg,`, `,`, `(Bob),`, `Smith, John,`, `John Smith, Jr.,`, `John Smith, PhD,`, `John`, `Mr. and Mrs.`; bound in `abdul`, `Abu`, `abd al`; next in `Dr.`, `Prof.`, `Prof`, `Bishop`, `King`, `King.`, `Sir`, `St`, `Freiherr`, `Salam`, `Rahman`, `Lt.Gov.`, `Mr.`, `van`, `MA`, `J.`; rest in (none), `Smith`, `Ali Smith`, `Dr.`, `Smith Dr.`, `Jr.`, `MA`, `Smith, Jr.`, `Prof. Smith` -- 5,184 texts under the three orders, comparing the seven fields and every report: 1,044 parses move, every one a title word into the title field and no report moved alone -- 660 in a part past the second comma (`John Smith, Jr., Abu Bishop Ali Smith` → title 'Bishop', as C2 reads `John Smith, Jr., Bishop`), the rest after a family comma or an empty head (`(Bob), Abu King.` → title 'King.', given 'Abu'). #623's given-part grid (decisions.md#H5): 280, the same class (`(Bob), abdul Dr.` → title 'Dr.', family 'abdul', H1's move behind a title). Every corpus name and case text at 4455962f: only `Berg, abdul Prof.`, the row that had pinned the gap. The S2 stress grid, #624's title-particle grid and #625's particle grid: 0. The five gates exit 0, with `fix(#637)` and `fix(#640)` rules in each 2.x ledger; at 1.4.0 the broad `fix(comma-family) lone post-comma piece routes to suffix/title` rule explains both new examples. Frames: the reference rows do not move; a name with a bound given word pays one for the title test (`abdul salam ahmed salem` 241 → 242), and one with a title word behind it after a family comma about ten to twenty for the H5 read (`Berg, abdul Bishop` 208 → 218, `Berg, abdul Prof.` 207 → 228), rare shapes no guard sees. Recorded negative controls: with no title declined, 5 tests fail (the row on both surfaces, H5's example and two P5 examples); with the past-the-second-comma half alone removed, the row `a_bound_join_declines_a_title_past_the_second_comma` (both surfaces) and one P5 example. ### C1 — the suffix-comma decision diff --git a/docs/design/rules.md b/docs/design/rules.md index 6edba211..69b02771 100644 --- a/docs/design/rules.md +++ b/docs/design/rules.md @@ -352,13 +352,11 @@ H5. Rationale: a word abbreviated with a period at the END of a name title absent — and lands in the same field, the run in FRONT of the joined pair being the run H1 asks about. Said of the comma-less writing, which is where the chain runs before the - reserve is read. After a family comma the reserve reads no peel - at all (P5), so the join there takes the trailing title word into - the given name and the chain never sees it, where the same - segment writing with an ordinary given name reads the title. A - gap in the segment path rather than a boundary of this rule, and - tracked as part of #316. Pinned by a case row rather than by an - example here, so recording the gap does not make it normative. + reserve is read. After a family comma the reserve reads no peel, + but the join declines a trailing title word the chain takes (P5), + so the title is read there too, as the segment written with an + ordinary given name reads it. + "Berg, abdul Prof." → title="Prof." 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 ## Particles & surname prefixes (P) @@ -723,7 +721,10 @@ P5. Rationale: some given-name words are incomplete alone — "abdul" them asks the question about a name that will not exist. The join never absorbs a marker standing as a word of its own — a marker is not a name word (M2) — nor a word of the unambiguous suffix - vocabulary (S2), wherever position will then place it; a bare + vocabulary (S2), wherever position will then place it, nor a + title word a trailing rule reads as a title: one the trailing + title run (H5) takes from the given part after a family comma, + or any title word in a part past the second comma (C2); a bare ambiguous acronym is a name word wherever the peel does not take it; a marker left as a word that a particle join (P2) has already taken travels with that join. A title word standing in the name @@ -772,6 +773,9 @@ P5. Rationale: some given-name words are incomplete alone — "abdul" "abdul Sir Smith Berg" → given="abdul Sir" "Berg, abdul van" → given="abdul van" "Berg, abdul vd" → family="vd Berg" + "Berg, abdul Prof." → given="abdul" + "Berg, abdul Prof. Ali" → given="abdul Prof." · boundary + "John Smith, Jr., abdul Dr. Smith" → title="Dr." "abdul Jr Smith Berg" → given="abdul" "abdul Jr Smith Berg" → middle="Jr Smith" "abdul Ph. D. Smith Berg" → suffix="Ph. D." diff --git a/docs/release_log.rst b/docs/release_log.rst index 49bb928c..c4185fd1 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -22,7 +22,7 @@ Release Log - **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) - - **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) + - **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.``. A bound given name there no longer takes a trailing title into it: ``HumanName("Berg, abdul Prof.")`` gives title ``Prof.``, first ``abdul``, where 1.4.0, 2.2.0 and 2.3.0 gave first ``abdul Prof.`` and 2.0.0 and 2.1.0 middle ``Prof.``. See the #614 entry under ``S2``, the #623 entry under ``H5`` and the #637 entry under ``P5`` in ``docs/design/decisions.md`` (closes #614, #623, #637) - **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) @@ -44,7 +44,7 @@ Release Log - **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) - - **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) + - **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.``. A bound given name no longer takes such a title word into it: ``HumanName("John Smith, Jr., abdul Dr. Smith")`` gives title ``Dr.``, suffix ``Jr., abdul Smith``. (#603, closes #629, #636, #640) - **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) diff --git a/nameparser/_pipeline/_group.py b/nameparser/_pipeline/_group.py index 99dbcc24..f5a5083f 100644 --- a/nameparser/_pipeline/_group.py +++ b/nameparser/_pipeline/_group.py @@ -1186,14 +1186,29 @@ def chain(tail: int) -> None: # vocabulary (S2), wherever position will then place it" # -- and declining it is also what keeps merge_pieces' tag # union from making the joined piece a suffix piece. - if (_is_maiden_marker_piece(pieces[fk + 1], tokens) - or is_suffix_piece(pieces[fk + 1], ptags[fk + 1], - tokens)): + # rules.md#P5: "nor a title word a trailing rule reads as a + # title" -- after a family comma one H5's trailing run takes + # from the given part, which the LENIENT join reads no peel + # for and so asks here, only where a title word follows + # ('Berg, abdul Prof.', #637); in a part past the second + # comma any title word, which C2 reads as a title ('John + # Smith, Jr., abdul Dr. Smith', #640). A title word standing + # in the name is a name word and joins ('abdul Sir Smith + # Berg', 'Berg, abdul Prof. Ali'). + nxt, nxt_tags = pieces[fk + 1], ptags[fk + 1] + if (_is_maiden_marker_piece(nxt, tokens) + or is_suffix_piece(nxt, nxt_tags, tokens) + or (is_title_piece(nxt, nxt_tags, tokens) + and (site is ClauseSite.TAIL + or (bound_join is BoundJoin.LENIENT + and fk + 1 in tail_reading( + peel_walk(fk, ptags), pieces, ptags, + tokens, one_case)[1])))): pass elif bound_join is BoundJoin.LENIENT: # post-comma the family is fixed and the pair is the # given whatever follows, so no peel is read - # (decisions.md#P5, #423) + # (decisions.md#P5, #423), save the title test above merge_pieces(pieces, ptags, fk, fk + 2, drop={"title"}) else: # rules.md#P5: "the trailing suffix run (S2) and the diff --git a/tests/v2/cases.py b/tests/v2/cases.py index 4b177de1..6aaeed7a 100644 --- a/tests/v2/cases.py +++ b/tests/v2/cases.py @@ -9797,20 +9797,28 @@ def _check_cjk_shape_purity(self) -> None: "(measured 2026-09-09)"), Case("title_word_trailing_behind_a_bound_pair_after_a_comma", "Berg, abdul Prof.", - {"given": "abdul Prof.", "family": "Berg"}, - notes="the recorded GAP in rules.md#H5's P5 clause, pinned " - "here rather than by an example there so that " - "recording it does not make it normative. After a " - "family comma the reserve reads no peel (rules.md#P5), " - "so the bound join fires over the trailing title word " - "and takes it into the given name; the trailing walk " - "never sees it. The comma-less spelling is " - "transparent -- 'Sir abdul rahman Prof.' above -- and " - "so is this segment with an ordinary given name, " - "'Smith, John Prof.' reading title 'Prof.'. PARITY, " - "which is why it is not a fix row: 1.4.0 read first " - "'abdul Prof.', last 'Berg' too (measured 2026-09-09). " - "Tracked as part of #316"), + {"title": "Prof.", "given": "abdul", "family": "Berg"}, + classification="fix(#637)", + notes="after a family comma the reserve reads no peel " + "(rules.md#P5), but the join declines a trailing title " + "word the H5 run takes, so the title is read as the " + "segment with an ordinary given name reads it ('Smith, " + "John Prof.'), and as the comma-less 'Sir abdul rahman " + "Prof.' above does. Until #637 the row pinned the gap: " + "given 'abdul Prof.', as 1.4.0 read it (measured " + "2026-09-09)"), + Case("a_bound_join_declines_a_title_past_the_second_comma", + "John Smith, Jr., abdul Dr. Smith", + {"title": "Dr.", "given": "John", "family": "Smith", + "suffix": "Jr., abdul Smith"}, + ambiguities=("comma-structure",), + classification="fix(#640)", + notes="a part past the second comma holds no name word, so " + "every title word in it is a title (rules.md#C2) and the " + "bound join declines one (rules.md#P5, #640), as a " + "family comma's tail always read ('Smith, John, abdul " + "Dr. Smith'); 1.4.0 and 2.3.0 read suffix 'Jr., abdul " + "Dr. Smith'"), Case("title_run_leading_addresses_over_a_trailing_title", "Sir John Prof.", {"title": "Sir Prof.", "given": "John"}, diff --git a/tests/v2/test_ledger_guards.py b/tests/v2/test_ledger_guards.py index c24ec4b9..7d2353fa 100644 --- a/tests/v2/test_ledger_guards.py +++ b/tests/v2/test_ledger_guards.py @@ -3853,6 +3853,7 @@ def _claim(rule: dict) -> _Claim: # 2026-10-06, #617: 454 -> 455; its reach absorbed the C1 example 'Smith, Chancellor of the Exchequer MP' #617 added to rules.md. # 2026-10-10, #623: 455 -> 457; its reach absorbed the two rules.md#H5 examples #623 added, 'Smith, John van der Berg Prof.' and 'Smith, Jan van Prof.', both explained by fix(#623). # 2026-10-11, #636: 457 -> 458; its reach absorbed the rules.md#C2 example 'John Smith, Jr., van der Berg Prof.' #636 added, which at 1.4.0 it explains (suffix and title), as fix(#636) does at 2.x. + # 2026-10-11, #637/#640: 458 -> 461; its reach absorbed the three rules.md#P5 examples #637/#640 added, 'Berg, abdul Prof.', 'Berg, abdul Prof. Ali' and 'John Smith, Jr., abdul Dr. Smith', the first and last of which it explains at 1.4.0, as fix(#637) and fix(#640) do at 2.x. "fix(comma-family) lone post-comma piece routes to suffix/title, not first": # 2026-10-01, #564: 423 -> 430, 'John Smith, XYZ', 'Smith, # XYZ', 'García Márquez, MJ', 'García Márquez, MJ PhD', @@ -3876,7 +3877,7 @@ def _claim(rule: dict) -> _Claim: # rules.md#H1 comma example. Reach. # 2026-10-04, merging #606 in as well: 441 over the merged # corpus. Reach. - _Claim(458, ('given', 'suffix', 'title'), "f7ff98eb54b9", None), + _Claim(461, ('given', 'suffix', 'title'), "496fb3966b46", None), "fix(comma-family) a comma followed only by titles keeps the given/family split": _Claim(2, ('family', 'given'), "5bd9c6d96c38", None), "fix(comma-family) a comma followed only by titles keeps the given/family split, the C1 example": @@ -3991,6 +3992,7 @@ def _claim(rule: dict) -> _Claim: # 2026-10-06, #617: 454 -> 455; its reach absorbed the C1 example 'Smith, Chancellor of the Exchequer MP' #617 added to rules.md. # 2026-10-10, #623: 455 -> 457; its reach absorbed the two rules.md#H5 examples #623 added, 'Smith, John van der Berg Prof.' and 'Smith, Jan van Prof.', both explained by fix(#623). # 2026-10-11, #636: 457 -> 458; its reach absorbed the rules.md#C2 example 'John Smith, Jr., van der Berg Prof.' #636 added, which at 1.4.0 it explains (suffix and title), as fix(#636) does at 2.x. + # 2026-10-11, #637/#640: 458 -> 461; its reach absorbed the three rules.md#P5 examples #637/#640 added, 'Berg, abdul Prof.', 'Berg, abdul Prof. Ali' and 'John Smith, Jr., abdul Dr. Smith', the first and last of which it explains at 1.4.0, as fix(#637) and fix(#640) do at 2.x. "fix(comma-precomma-family) pre-comma run reads as family, not given": # 2026-10-01, #564: 423 -> 430, 'John Smith, XYZ', 'Smith, # XYZ', 'García Márquez, MJ', 'García Márquez, MJ PhD', @@ -4008,7 +4010,7 @@ def _claim(rule: dict) -> _Claim: # 2026-10-04, #606: 443 -> 444, 'Greve, Anna'. Reach. # 2026-10-04, merging #606 in as well: 441 over the merged # corpus. Reach. - _Claim(458, ('family', 'given'), "f7ff98eb54b9", None), + _Claim(461, ('family', 'given'), "496fb3966b46", None), # 2026-10-01, #575: new, 4; 'De La Cruz, Ed', 'Freiherr von # Berg, Ed', 'Van Buren, Ed', 'de la Cruz, Ma'. "fix(#575) a particle surname before a comma is one name word": @@ -4301,8 +4303,9 @@ def _claim(rule: dict) -> _Claim: # another opening bound-given word. Reach again. # 2026-10-01, #575: 44 -> 45, 'abdul Salam, Ed'. Reach. # 2026-10-04, #601/#602: 45 -> 43; the rules.md#M2 examples #601 retired left the corpus, and the names #601 moved left this rule's regex. + # 2026-10-11, #637/#640: 43 -> 46, the three rules.md#P5 examples #637/#640 added ('Berg, abdul Prof.', 'Berg, abdul Prof. Ali', 'John Smith, Jr., abdul Dr. Smith'). Reach: where their roles move, `_initials` is not compared. "fix(initials-per-word) a bound-given run initials each word (facade, since 2.0.0)": - _Claim(43, ('_initials',), "de26d994148e", ('DEFAULT',)), + _Claim(46, ('_initials',), "a91bd719adc6", ('DEFAULT',)), # 2026-09-18: 109 -> 110. One new corpus name, # 'john van der berg ma' -- rules.md#P2's one-case contrast, # and a particle chain like every other member. @@ -5404,6 +5407,10 @@ def _claim(rule: dict) -> _Claim: _Claim(2, ('_ambiguities', 'family', 'middle', 'title'), "bbab0baa7826", None), "fix(#636) a part past the second comma makes no particle chain": _Claim(1, ('suffix', 'title'), "f409b04e4c16", None), + "fix(#637) the bound join declines a trailing title after a family comma": + _Claim(1, ('middle', 'title'), "feff01f1e769", None), + "fix(#640) the bound join declines a title word past the second comma": + _Claim(1, ('suffix', 'title'), "bd68517718e5", None), "fix(#601/#602) a credential in the clause starts a run the take consumes": _Claim(1, ('_ambiguities', 'family', 'middle', 'suffix'), "acdcc81cb17c", None), "fix(#601) a marker behind a connective joins the connective run, and the run's initials follow": @@ -5889,6 +5896,10 @@ def _claim(rule: dict) -> _Claim: _Claim(2, ('_ambiguities', 'family', 'middle', 'title'), "bbab0baa7826", None), "fix(#636) a part past the second comma makes no particle chain": _Claim(1, ('suffix', 'title'), "f409b04e4c16", None), + "fix(#637) the bound join declines a trailing title after a family comma": + _Claim(1, ('given', 'title'), "feff01f1e769", None), + "fix(#640) the bound join declines a title word past the second comma": + _Claim(1, ('suffix', 'title'), "bd68517718e5", None), "fix(#601/#602) a credential in the clause starts a run the take consumes": _Claim(1, ('_ambiguities', 'family', 'middle', 'suffix'), "acdcc81cb17c", None), # 2026-10-04, #604: new, 3; the rules.md#P7 boundary and the @@ -6607,6 +6618,10 @@ def _claim(rule: dict) -> _Claim: _Claim(2, ('_ambiguities', 'family', 'middle', 'title'), "bbab0baa7826", None), "fix(#636) a part past the second comma makes no particle chain": _Claim(1, ('suffix', 'title'), "f409b04e4c16", None), + "fix(#637) the bound join declines a trailing title after a family comma": + _Claim(1, ('middle', 'title'), "feff01f1e769", None), + "fix(#640) the bound join declines a title word past the second comma": + _Claim(1, ('suffix', 'title'), "bd68517718e5", None), "fix(#601/#602) a credential in the clause starts a run the take consumes": _Claim(1, ('_ambiguities', 'family', 'middle', 'suffix'), "acdcc81cb17c", None), "fix(#601) a marker behind a connective joins the connective run, and the run's initials follow": @@ -6945,6 +6960,10 @@ def _claim(rule: dict) -> _Claim: _Claim(2, ('_ambiguities', 'family', 'middle', 'title'), "bbab0baa7826", None), "fix(#636) a part past the second comma makes no particle chain": _Claim(1, ('suffix', 'title'), "f409b04e4c16", None), + "fix(#637) the bound join declines a trailing title after a family comma": + _Claim(1, ('given', 'title'), "feff01f1e769", None), + "fix(#640) the bound join declines a title word past the second comma": + _Claim(1, ('suffix', 'title'), "bd68517718e5", None), "fix(#601/#602) a credential in the clause starts a run the take consumes": _Claim(1, ('_ambiguities', 'family', 'middle', 'suffix'), "acdcc81cb17c", None), # 2026-10-04, #604: new, 3; the rules.md#P7 boundary and the diff --git a/tools/differential/corpus_rules.jsonl b/tools/differential/corpus_rules.jsonl index 8439833e..c625064c 100644 --- a/tools/differential/corpus_rules.jsonl +++ b/tools/differential/corpus_rules.jsonl @@ -29,6 +29,8 @@ "Berg, Jan van der" "Berg, Jan vd" "Berg, abd née Jones" +"Berg, abdul Prof." +"Berg, abdul Prof. Ali" "Berg, abdul V" "Berg, abdul van" "Berg, abdul vd" @@ -216,6 +218,7 @@ "John Smith, Jr do" "John Smith, Jr vd" "John Smith, Jr., Lt.Gov." +"John Smith, Jr., abdul Dr. Smith" "John Smith, Jr., van der Berg Prof." "John Smith, LEED AP" "John Smith, MA" diff --git a/tools/differential/expected_since_2.0.0.toml b/tools/differential/expected_since_2.0.0.toml index 4d7ecb98..86d80629 100644 --- a/tools/differential/expected_since_2.0.0.toml +++ b/tools/differential/expected_since_2.0.0.toml @@ -3950,6 +3950,22 @@ issue = "fix(#636) a part past the second comma makes no particle chain" name_regex = "^John Smith, Jr\\., van der Berg Prof\\.$" fields = ["suffix", "title"] +[[change]] +# rules.md#P5: "title word a trailing rule reads as a title" (#637, +# 2026-10-11): after a family comma the bound join declines the +# trailing title word H5's run takes, so it is read as the title. +issue = "fix(#637) the bound join declines a trailing title after a family comma" +name_regex = "^Berg, abdul Prof\\.$" +fields = ["middle", "title"] + +[[change]] +# rules.md#P5: "title word a trailing rule reads as a title" (#640, +# 2026-10-11): in a part past the second comma the bound join declines +# a title word, which C2 reads as a title. +issue = "fix(#640) the bound join declines a title word past the second comma" +name_regex = "^John Smith, Jr\\., abdul Dr\\. Smith$" +fields = ["suffix", "title"] + [[change]] # rules.md#M2 reads the clause-free name, whose credential starts #602's # run -- rules.md#S2: "A credential after the name core starts a run to diff --git a/tools/differential/expected_since_2.1.0.toml b/tools/differential/expected_since_2.1.0.toml index 123af67d..ee27872b 100644 --- a/tools/differential/expected_since_2.1.0.toml +++ b/tools/differential/expected_since_2.1.0.toml @@ -3895,6 +3895,22 @@ issue = "fix(#636) a part past the second comma makes no particle chain" name_regex = "^John Smith, Jr\\., van der Berg Prof\\.$" fields = ["suffix", "title"] +[[change]] +# rules.md#P5: "title word a trailing rule reads as a title" (#637, +# 2026-10-11): after a family comma the bound join declines the +# trailing title word H5's run takes, so it is read as the title. +issue = "fix(#637) the bound join declines a trailing title after a family comma" +name_regex = "^Berg, abdul Prof\\.$" +fields = ["middle", "title"] + +[[change]] +# rules.md#P5: "title word a trailing rule reads as a title" (#640, +# 2026-10-11): in a part past the second comma the bound join declines +# a title word, which C2 reads as a title. +issue = "fix(#640) the bound join declines a title word past the second comma" +name_regex = "^John Smith, Jr\\., abdul Dr\\. Smith$" +fields = ["suffix", "title"] + [[change]] # rules.md#M2 reads the clause-free name, whose credential starts #602's # run -- rules.md#S2: "A credential after the name core starts a run to diff --git a/tools/differential/expected_since_2.2.0.toml b/tools/differential/expected_since_2.2.0.toml index a9c4891b..4898c0b3 100644 --- a/tools/differential/expected_since_2.2.0.toml +++ b/tools/differential/expected_since_2.2.0.toml @@ -2391,6 +2391,22 @@ issue = "fix(#636) a part past the second comma makes no particle chain" name_regex = "^John Smith, Jr\\., van der Berg Prof\\.$" fields = ["suffix", "title"] +[[change]] +# rules.md#P5: "title word a trailing rule reads as a title" (#637, +# 2026-10-11): after a family comma the bound join declines the +# trailing title word H5's run takes, so it is read as the title. +issue = "fix(#637) the bound join declines a trailing title after a family comma" +name_regex = "^Berg, abdul Prof\\.$" +fields = ["given", "title"] + +[[change]] +# rules.md#P5: "title word a trailing rule reads as a title" (#640, +# 2026-10-11): in a part past the second comma the bound join declines +# a title word, which C2 reads as a title. +issue = "fix(#640) the bound join declines a title word past the second comma" +name_regex = "^John Smith, Jr\\., abdul Dr\\. Smith$" +fields = ["suffix", "title"] + [[change]] # rules.md#M2 reads the clause-free name, whose credential starts #602's # run -- rules.md#S2: "A credential after the name core starts a run to diff --git a/tools/differential/expected_since_2.3.0.toml b/tools/differential/expected_since_2.3.0.toml index 2103b61d..9395700a 100644 --- a/tools/differential/expected_since_2.3.0.toml +++ b/tools/differential/expected_since_2.3.0.toml @@ -1672,6 +1672,22 @@ issue = "fix(#636) a part past the second comma makes no particle chain" name_regex = "^John Smith, Jr\\., van der Berg Prof\\.$" fields = ["suffix", "title"] +[[change]] +# rules.md#P5: "title word a trailing rule reads as a title" (#637, +# 2026-10-11): after a family comma the bound join declines the +# trailing title word H5's run takes, so it is read as the title. +issue = "fix(#637) the bound join declines a trailing title after a family comma" +name_regex = "^Berg, abdul Prof\\.$" +fields = ["given", "title"] + +[[change]] +# rules.md#P5: "title word a trailing rule reads as a title" (#640, +# 2026-10-11): in a part past the second comma the bound join declines +# a title word, which C2 reads as a title. +issue = "fix(#640) the bound join declines a title word past the second comma" +name_regex = "^John Smith, Jr\\., abdul Dr\\. Smith$" +fields = ["suffix", "title"] + [[change]] # rules.md#M2 reads the clause-free name, whose credential starts #602's # run -- rules.md#S2: "A credential after the name core starts a run to From 1e5dfa9f4f0470b911e53fb33949ba9d80b8d754 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sun, 11 Oct 2026 04:05:14 -0700 Subject: [PATCH 2/3] Fix #643's review: record the decline's limits, pin two mechanisms The decline after a family comma asks H5's run as the comma-less name reads it, and the given part's walk reads some words behind a title otherwise (a numeral VI to X, V before a third part, a title-particle P6 attaches): there the join is declined and no title is read. Garbage shapes, recorded rather than narrowed as #623's disagreement was, and stated in an Accepted clause of rules.md#P5; the entry no longer says every move gains a title, and its #623-grid count names the 20 that do not. Rows pin the bare-title half past the second comma and the whole trailing run after a family comma, which no test held. Corrected: option (a)'s count (3,000 joins split, not 1,944), the negative controls (11 and 5), a row note's history (#603), a ledger note naming the wrong rule, the entry's place in the P5 section, notes on the #623 and #614 entries, P5/C2 interacts, two release sentences. The site is asked before the title test, so a comma-less bound name pays no frame for it. Co-Authored-By: Claude Opus 5.5 --- docs/design/decisions.md | 12 ++++++------ docs/design/rules.md | 15 ++++++++++++--- docs/release_log.rst | 4 ++-- nameparser/_pipeline/_group.py | 26 ++++++++++++++++---------- tests/v2/cases.py | 27 ++++++++++++++++++++++++--- tests/v2/test_ledger_guards.py | 4 ++-- 6 files changed, 62 insertions(+), 26 deletions(-) diff --git a/docs/design/decisions.md b/docs/design/decisions.md index 75977fb2..e982e2c0 100644 --- a/docs/design/decisions.md +++ b/docs/design/decisions.md @@ -609,7 +609,7 @@ Decided 2026-09-08 (was Open: [#316](https://github.com/derek73/python-nameparse - 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. DECIDED (Derek): the narrower bound. 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. - 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 shapes 38 to 73 in a sample (`Smith, John van der Berg Prof. PhD` +38, `Smith, John van der Berg Prof.` 310 → 360, `Smith, Jan van Prof.` +73), with an unmoved particle name ending in a suffix and a title 20 to 22 (`Smith, John van der Berg PhD Prof.` 382 → 402, `Smith, John van der Berg Jr. Prof.` 382 → 404), rare shapes 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. + 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 (fixed by #637, decisions.md#P5, 2026-10-11). 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 shapes 38 to 73 in a sample (`Smith, John van der Berg Prof. PhD` +38, `Smith, John van der Berg Prof.` 310 → 360, `Smith, Jan van Prof.` +73), with an unmoved particle name ending in a suffix and a title 20 to 22 (`Smith, John van der Berg PhD Prof.` 382 → 402, `Smith, John van der Berg Jr. Prof.` 382 → 404), rare shapes 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. 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, and over a 134,720-pair transparency grid (`Smith, ` and `, ` heads; one to three words from `John`, `van`, `der`, `von und zu`, `Berg`, `Ma`, `MA`, `V`, `VI`, `Jr.`, `PhD`, `Ph. D.`, `Mc`, `St.`, `St`, `Freiherr`, `X.Y.Z.`, `Esq.`, `abdul`, `Prof.`; a trailing ` Prof.`, with and without `, Jr.` after it; the three orders and `lenient_comma_suffixes=False`; transparent where given, middle, family, nickname and maiden equal the title-less text's and the title gains exactly ` Prof.`; master broken on 53,384 pairs, the tree on 9,734), no text transparent to its title on master is broken now. (The first review named `Smith, John Ph. D. St. Prof.` as one, its oracle reading the direction backwards: master gives suffix 'Ph. D. St. Prof.' against the title-less 'family St. Smith', and the tree is transparent.) 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`) still decides readings, though its documented case (`Freiherr von Berg Ma`) takes the read-once path since #614: removed, it moves 400 parses of the given-part grid on master and here alike, a title-particle head before a particle run and a dotted acronym (`Smith, Freiherr van Berg X.Y.Z.` → given 'van Berg X.Y.Z.', as 1.4.0 reads it; without it suffix 'X.Y.Z.'). It decides a main segment's run with a name piece inside it too (`Freiherr van Berg Dr. X.Y.Z.` → family 'van Berg Dr. X.Y.Z.', as master and 2.3.0 read it; without it given 'van Berg Dr.', family 'X.Y.Z.', 1.4.0's reading), a garbage shape no row holds. The first review reported it changing nothing over a grid with no such shape, and #638 was filed on that; the fix review found it live and held by no test, and the row `the_chain_runs_again_where_its_merges_free_the_acronym` (parity) holds it now. Left for follow-ups, 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.'; #636), and P5's post-comma join taking a title (the 40 above; #637). ### W1 — unspaced CJK division @@ -888,7 +888,11 @@ Closes #469, and continues the corpus-tier arc below rather than standing apart One principle came out of writing the ledger that the view alone did not state. `abdul Smith Ma` had read given 'abdul Smith', family 'Ma' since 2.0, and the #423 entry called that the lossless shape; the 1.4.0 gate's catch-all count said otherwise — 1.4.0 reads first 'abdul', last 'Smith', suffix 'Ma', as `John Smith Ma` reads, and the fields-only catch-all had absorbed the diff since the example went in (the count is the tell, per the #401 entry). Unjoined, the acronym has words to spare and peels as a credential; joined, the view is two pieces and the fork keeps it as the family — the join was CHANGING what assign reads. So the rule is stated as P5 now has it: the join joins two name words into one and changes nothing else, which the code asks directly: the pieces the peel takes are the same either way (a count proxy, "the view leaves exactly one name word fewer", was the first spelling; the altitude review pointed out it held only because the peel is a tail walk, and the comparison states the rule). That comparison subsumes the unjoined-piece check #423 added for `abdul V` (unjoined the peel takes the V, joined nothing) and restores `abdul Smith Ma` to 1.4.0's reading; `abdul Smith Berg Ma` keeps its join, the acronym peeling either way. Every #423 pin holds. Measured: the 1.4.0 gate is byte-identical at the code change, before the examples went in — the three `Ma`-final examples are v1 parity, the catch-all back at 15 — and `abdul Smith Jr Ma` and `abdul Smith Ma` move against 2.0.0 and 2.1.0, classified there; no hand corpus has the shapes. The population the corpus cannot see, from the design-docs review's sweep of 67,860 constructed main-walk shapes (three bound words — abdul, abd, abu — × twelve words — Smith, Berg, Ed, J., Jr, M.A., Ma, PhD, V, X, 'Ph. D.', 'van der', none a title word — × tails of one to four; no comma shapes; a different twelve-word pool gives different counts, so the pool is the claim): before this change 2,160 lost the family and every one contained a bare ambiguous acronym (864 with further suffixes behind it, so "ending in" was an artefact of #423's shorter sweep); after it none does, and all 67,860 match the family and suffix their ordinary-given twin reads. 5,166 move against master in all — 3,006 where the acronym was the family and is now the suffix (`Sir abdul Smith Ma`), 2,160 where the family is restored. The title-word class below was outside that population and moves after a family comma too (`Berg, abdul Sir`); so was the licence class at the end of this entry. - Three things the principle does not reach, recorded so nobody applies it there. The post-comma LENIENT path is untouched, and NOT for the reason first written here ("its walk has no peel" — it reads a trailing numeral by the lenient last-of-two rule, as the #423 entry says): the family is fixed there and the pair is the given whatever follows, so the principle is not applied, and `Berg, abdul V` keeps given 'abdul V' though the V would be the suffix unjoined — 1.4.0 parity, and exactly the regression #423 measured when a mirror was applied there; P5 now carries it as Accepted. The vocabulary decline is the UNAMBIGUOUS vocabulary: a bare ambiguous acronym the peel does not take is a name word, so `abdul Ma Smith` reads given 'abdul Ma' (1.4.0 read middle 'Ma' — a 2.x difference no corpus witnesses, now an Accepted example). And the principle is about the suffix reading, not the report: `Sheik abdul Ma` reports no SUFFIX_OR_NAME where `Sheik John Ma` does, the #369 precedent — a bound word read as the bound word is not a fork. One more class moves, found by the code review and then measured against 1.4.0: the 2.x reserve had excluded TITLE pieces from its count — an unrecorded deviation from v1, whose join counted suffixes out and nothing else — and the shared peel, following assign, reads a mid-name title word as the name word it is. `Berg, abdul Sir` (given 'abdul', middle 'Sir' since 2.0) and `abdul Smith Sir` (declined since 2.0) read given 'abdul Sir' and given 'abdul Smith', family 'Sir' — 1.4.0's readings, both; `abdul Sir Smith Berg` never stopped reading given 'abdul Sir'. No corpus name has the shapes that move, and no rules example carries one — `abdul Sir Smith Berg`, P5's example of the reading, is byte-identical at every baseline, so the class is pinned in tests alone (the release log says so; the late review of this entry found the sentence here implying otherwise). The same review found the clause had taken `do`, `freiherr` and `st` from P6 — recorded under P6. What the review actually caught was the tag artefact that exclusion had been hiding: the conjunction merge derives a `title` piece tag for a chain such as "Sheikh and Ahmad", merge()'s union handed it to the pair, and assign peeled the pair as a leading title — `abdul Sheikh and Ahmad Bakar Smith` read title 'abdul Sheikh and Ahmad' on 2.0 and 2.1 (v1 read it as the given), and `abdul Sheikh and Ahmad Bakar`, which the exclusion had kept unjoined, would have too once it went. The bound join drops the tag, and the view drops it the same way — the pair is a given name whatever tag the word carried. One more class the comparison moves, found by the code review's classification of every residual master-to-branch diff (12 of 17,640 constructed names — a fourteen-word pool, bare and behind `Sir`, tails of two and three, 6 × (14² + 14³); the review did not record the pool — one shape; 360 of 360 over {Sir, Sheikh, Dame, Sheik, King, Father} × {abdul, abd, abu, umm} × {J., V, X, A., I} × {V, X, I}): behind a given-name title, a numeral after an initial-shaped piece. `Sir abdul J. V` read given 'abdul J.', suffix 'V' since #369 — the fork suppressed unjoined, fired joined, and the licence's threshold of one let the join through — and the comparison declines it: given 'abdul', middle 'J.', family 'V', exactly as `Sir John J. V`. Pinned at both levels; it was the only shape a looser comparison let through under mutation at the test review. #424 — P2's chain and M2's walk taking a trailing numeral — is the remaining asker of the same question and now has `_peel_trailing` to ask it of. + Three things the principle does not reach, recorded so nobody applies it there. The post-comma LENIENT path is untouched (AMENDED by #637, below: its decline now reads H5's trailing run where a title word follows the bound word), and NOT for the reason first written here ("its walk has no peel" — it reads a trailing numeral by the lenient last-of-two rule, as the #423 entry says): the family is fixed there and the pair is the given whatever follows, so the principle is not applied, and `Berg, abdul V` keeps given 'abdul V' though the V would be the suffix unjoined — 1.4.0 parity, and exactly the regression #423 measured when a mirror was applied there; P5 now carries it as Accepted. The vocabulary decline is the UNAMBIGUOUS vocabulary: a bare ambiguous acronym the peel does not take is a name word, so `abdul Ma Smith` reads given 'abdul Ma' (1.4.0 read middle 'Ma' — a 2.x difference no corpus witnesses, now an Accepted example). And the principle is about the suffix reading, not the report: `Sheik abdul Ma` reports no SUFFIX_OR_NAME where `Sheik John Ma` does, the #369 precedent — a bound word read as the bound word is not a fork. One more class moves, found by the code review and then measured against 1.4.0: the 2.x reserve had excluded TITLE pieces from its count — an unrecorded deviation from v1, whose join counted suffixes out and nothing else — and the shared peel, following assign, reads a mid-name title word as the name word it is. `Berg, abdul Sir` (given 'abdul', middle 'Sir' since 2.0) and `abdul Smith Sir` (declined since 2.0) read given 'abdul Sir' and given 'abdul Smith', family 'Sir' — 1.4.0's readings, both; `abdul Sir Smith Berg` never stopped reading given 'abdul Sir'. No corpus name has the shapes that move, and no rules example carries one — `abdul Sir Smith Berg`, P5's example of the reading, is byte-identical at every baseline, so the class is pinned in tests alone (the release log says so; the late review of this entry found the sentence here implying otherwise). The same review found the clause had taken `do`, `freiherr` and `st` from P6 — recorded under P6. What the review actually caught was the tag artefact that exclusion had been hiding: the conjunction merge derives a `title` piece tag for a chain such as "Sheikh and Ahmad", merge()'s union handed it to the pair, and assign peeled the pair as a leading title — `abdul Sheikh and Ahmad Bakar Smith` read title 'abdul Sheikh and Ahmad' on 2.0 and 2.1 (v1 read it as the given), and `abdul Sheikh and Ahmad Bakar`, which the exclusion had kept unjoined, would have too once it went. The bound join drops the tag, and the view drops it the same way — the pair is a given name whatever tag the word carried. One more class the comparison moves, found by the code review's classification of every residual master-to-branch diff (12 of 17,640 constructed names — a fourteen-word pool, bare and behind `Sir`, tails of two and three, 6 × (14² + 14³); the review did not record the pool — one shape; 360 of 360 over {Sir, Sheikh, Dame, Sheik, King, Father} × {abdul, abd, abu, umm} × {J., V, X, A., I} × {V, X, I}): behind a given-name title, a numeral after an initial-shaped piece. `Sir abdul J. V` read given 'abdul J.', suffix 'V' since #369 — the fork suppressed unjoined, fired joined, and the licence's threshold of one let the join through — and the comparison declines it: given 'abdul', middle 'J.', family 'V', exactly as `Sir John J. V`. Pinned at both levels; it was the only shape a looser comparison let through under mutation at the test review. #424 — P2's chain and M2's walk taking a trailing numeral — is the remaining asker of the same question and now has `_peel_trailing` to ask it of. +- 2026-10-11 (Derek), #637 and #640 — THE BOUND JOIN DECLINES A TITLE WORD A TRAILING RULE READS AS A TITLE. P5 joins a bound given-name word to the word after it, and declined a maiden marker and a suffix piece; a title word it took, so where a trailing rule would have read that word as a title the joined piece was no title word to read. After a family comma the join reads no peel (the LENIENT reserve, #423), so `Berg, abdul Prof.` read given 'abdul Prof.' where `Berg, John Prof.` reads title 'Prof.' (#637, the gap the 2026-09-09 A1 entry recorded); in a part past the second comma after a suffix comma, where the join runs STRICT, `John Smith, Jr., abdul Dr. Smith` read suffix 'Jr., abdul Dr. Smith' where `John Smith, Jr., Berg Dr. Smith` reads title 'Dr.' (#640). Now the join declines a title word a trailing rule reads as a title: after a family comma, one H5's trailing run takes from the given part (`tail_reading`, asked only when a title word follows the bound word), and in a part past the second comma any title word, which C2 reads as a title. `Berg, abdul Prof.` → title 'Prof.', given 'abdul'; `John Smith, Jr., abdul Dr. Smith` → title 'Dr.'. + DECIDED (Derek): this narrower clause (a′) rather than declining every title word (a). P5 states that a title word standing in the name is a name word and joins like one (H3), and (a) undid it: it failed six tests, a P5 rules example and the two tests named for that clause among them, and over the bound-given grid below moved 4,044 parses, 3,000 of them a join split with no title read (`(Bob), Abu Bishop` → given 'Abu', middle 'Bishop') beside the 1,044 that (a′) moves (six tests measured against 4455962f's suite). (a′) leaves every join on a title word inside the name standing -- `abdul Sir Smith Berg`, `Berg, abdul Prof. Ali`, `Smith, abdul Bishop`. + MEASURED 2026-10-11 against 4455962f (#639's merge), py3.11, `nameparser.__file__` asserted on each side. A bound-given grid -- `{head} {bound} {next} {rest}` for head in (none), `Dr.`, `Sir`, `Smith,`, `van der Berg,`, `,`, `(Bob),`, `Smith, John,`, `John Smith, Jr.,`, `John Smith, PhD,`, `John`, `Mr. and Mrs.`; bound in `abdul`, `Abu`, `abd al`; next in `Dr.`, `Prof.`, `Prof`, `Bishop`, `King`, `King.`, `Sir`, `St`, `Freiherr`, `Salam`, `Rahman`, `Lt.Gov.`, `Mr.`, `van`, `MA`, `J.`; rest in (none), `Smith`, `Ali Smith`, `Dr.`, `Smith Dr.`, `Jr.`, `MA`, `Smith, Jr.`, `Prof. Smith` -- 5,184 texts under the three orders, comparing the seven fields and every report: 1,044 parses move, every one a title word into the title field and no report moved alone -- 660 in a part past the second comma (`John Smith, Jr., Abu Bishop Ali Smith` → title 'Bishop', as C2 reads `John Smith, Jr., Bishop`), the rest after a family comma or an empty head (`(Bob), Abu King.` → title 'King.', given 'Abu'). #623's given-part grid (decisions.md#H5): 280 -- 260 the same class (`(Bob), abdul Dr.` → title 'Dr.', family 'abdul', H1's move behind a title), and 20 that split the join with no title read (`Smith, abdul Prof. V, Jr.` → given 'abdul', middle 'Prof. V'), the class below. Every corpus name and case text at 4455962f: only `Berg, abdul Prof.`, the row that had pinned the gap. The S2 stress grid, #624's title-particle grid and #625's particle grid: 0. The five gates exit 0, with `fix(#637)` and `fix(#640)` rules in each 2.x ledger; at 1.4.0 the broad `fix(comma-family) lone post-comma piece routes to suffix/title` rule explains both new examples. Frames: the reference rows do not move; a comma-less name with a bound given word pays nothing, the site asked before the title test (`abdul salam ahmed salem` 241), and one with a title word behind it after a family comma about ten to twenty for the H5 read (`Berg, abdul Bishop` 208 → 218, `Berg, abdul Prof.` 207 → 228), rare shapes no guard sees. Recorded negative controls (with the review's rows): with no title declined, 11 tests fail (the four #637/#640 rows on both surfaces, H5's example and two P5 examples); with the past-the-second-comma half alone removed, 5 (its two rows on both surfaces and one P5 example). + THE REVIEW (2026-10-11). The decline after a family comma asks H5's run as the comma-less name reads it (`tail_reading`), and the given part's own walk reads some words behind a title otherwise, the disagreement #623's entry (decisions.md#H5) records for the chain's bound: there the join is declined and no title is read. A shape-only numeral VI to X, or V before a third comma part, which the walk keeps a name word (`Smith, abdul Prof. VI` → given 'abdul', middle 'Prof. VI', where master and 1.4.0 read given 'abdul Prof.', middle 'VI'); a period-marked title that is also a particle, which P6 then attaches with its report (`Smith, abdul St.` → family 'St. Smith', particle-or-given); and a bound word that is also a particle before one (`Smith, Abu St.` → title 'St.', where `Smith, Zed St.` reads family 'St. Smith'). Every such shape is garbage; the first two read as the same name with an ordinary given word does. Recorded, not narrowed, as #623's disagreement was (Derek, #623), and rules.md#P5 states it as an Accepted clause; the review's sweep found nothing reading worse than master on a real name and no comma-less name moving. It also found two mechanisms no test held, now rows: the bare-title half past the second comma (`John Smith, Jr., abdul Sir Smith`) and the whole trailing run after a family comma rather than the last word (`Berg, abdul Prof. Dr.`). Excluded (BOUND_GIVEN_NAMES): @@ -954,10 +958,6 @@ Excluded (MAIDEN_MARKERS, per nameparser/config/maiden_markers.py): - Polish "z domu" — INCLUDED 2026-08-26 as the set's first phrase entry, and the contrast to draw against the roz removal below rather than against any included word. Its words fail C-i separately and it passes as a pair; the argument is in the #434 entry under #M2. The point to carry away is that the entry below could not have been rescued the same way: roz is a whole marker as written, so there is no longer unit to move the claim onto. - Czech/Slovak "roz." — SHIPPED through 2.1 and removed 2026-08-26 (Derek's call). Roz is an ordinary English diminutive of Rosalind, and matching is whole-token, case-folded and period-insensitive, so "Roz", "roz" and "roz." are one string to this set. The collision is not theoretical and it is not new: on the bare path, which predates the 2.2 work entirely, "Rosalind Roz Smith" read maiden "Smith" and NO family name at all, and "Rosalind Roz Jones Smith" read maiden "Jones Smith", because M2 hands the marker every word after it. Grounded in #vocabulary-collisions C-i — a word belongs in its set's ambiguous subset iff it is borne as an ordinary name IN THE POSITION THE VOCABULARY CLAIM ACTS ON, and under uncertainty default to ambiguous. Roz is borne exactly there: the word after a given name, which is the only position M2's claim acts on. What C-i cannot do for this set is the marking: MAIDEN_MARKERS has no ambiguous subset, and no code path consults one, so the only two expressions of C-i available here are ship and do not ship. That is why the remedy is removal rather than an ambiguous marking, and it is worth knowing before someone proposes marking the next collision instead. The full participle rozená stays and is unambiguous; a Czech caller who needs the abbreviation adds it to their own Lexicon, which is the same answer this section gives for every locale-specific vocabulary. The cost is accepted and stated: "Anna Nováková roz. Svobodová" now reads middle "Nováková roz.", family "Svobodová" — which is how 1.4.0 read it — and "Rosalind Roz Smith" is back to 1.4.0's given/middle/family. Pinned by tests/v2/cases.py::diminutive_that_was_a_marker_keeps_the_family and ::full_participle_marker_still_consumes. -- 2026-10-11 (Derek), #637 and #640 — THE BOUND JOIN DECLINES A TITLE WORD A TRAILING RULE READS AS A TITLE. P5 joins a bound given-name word to the word after it, and declined a maiden marker and a suffix piece; a title word it took, so where a trailing rule would have read that word as a title the joined piece was no title word to read. After a family comma the join reads no peel (the LENIENT reserve, #423), so `Berg, abdul Prof.` read given 'abdul Prof.' where `Berg, John Prof.` reads title 'Prof.' (#637, the gap the 2026-09-09 A1 entry recorded); in a part past the second comma after a suffix comma, where the join runs STRICT, `John Smith, Jr., abdul Dr. Smith` read suffix 'Jr., abdul Dr. Smith' where `John Smith, Jr., Berg Dr. Smith` reads title 'Dr.' (#640). Now the join declines a title word a trailing rule reads as a title: after a family comma, one H5's trailing run takes from the given part (`tail_reading`, asked only when a title word follows the bound word), and in a part past the second comma any title word, which C2 reads as a title. `Berg, abdul Prof.` → title 'Prof.', given 'abdul'; `John Smith, Jr., abdul Dr. Smith` → title 'Dr.'. - DECIDED (Derek): this narrower clause (a′) rather than declining every title word (a). P5 states that a title word standing in the name is a name word and joins like one (H3), and (a) undid it: it failed six tests, a P5 rules example and the two tests named for that clause among them, and over the bound-given grid below moved about 4,000 parses, 1,944 of them a real join split (`(Bob), Abu Bishop` → given 'Abu', middle 'Bishop'). (a′) leaves every join on a title word inside the name standing -- `abdul Sir Smith Berg`, `Berg, abdul Prof. Ali`, `Smith, abdul Bishop`. - MEASURED 2026-10-11 against 4455962f (#639's merge), py3.11, `nameparser.__file__` asserted on each side. A bound-given grid -- `{head} {bound} {next} {rest}` for head in (none), `Dr.`, `Sir`, `Smith,`, `van der Berg,`, `,`, `(Bob),`, `Smith, John,`, `John Smith, Jr.,`, `John Smith, PhD,`, `John`, `Mr. and Mrs.`; bound in `abdul`, `Abu`, `abd al`; next in `Dr.`, `Prof.`, `Prof`, `Bishop`, `King`, `King.`, `Sir`, `St`, `Freiherr`, `Salam`, `Rahman`, `Lt.Gov.`, `Mr.`, `van`, `MA`, `J.`; rest in (none), `Smith`, `Ali Smith`, `Dr.`, `Smith Dr.`, `Jr.`, `MA`, `Smith, Jr.`, `Prof. Smith` -- 5,184 texts under the three orders, comparing the seven fields and every report: 1,044 parses move, every one a title word into the title field and no report moved alone -- 660 in a part past the second comma (`John Smith, Jr., Abu Bishop Ali Smith` → title 'Bishop', as C2 reads `John Smith, Jr., Bishop`), the rest after a family comma or an empty head (`(Bob), Abu King.` → title 'King.', given 'Abu'). #623's given-part grid (decisions.md#H5): 280, the same class (`(Bob), abdul Dr.` → title 'Dr.', family 'abdul', H1's move behind a title). Every corpus name and case text at 4455962f: only `Berg, abdul Prof.`, the row that had pinned the gap. The S2 stress grid, #624's title-particle grid and #625's particle grid: 0. The five gates exit 0, with `fix(#637)` and `fix(#640)` rules in each 2.x ledger; at 1.4.0 the broad `fix(comma-family) lone post-comma piece routes to suffix/title` rule explains both new examples. Frames: the reference rows do not move; a name with a bound given word pays one for the title test (`abdul salam ahmed salem` 241 → 242), and one with a title word behind it after a family comma about ten to twenty for the H5 read (`Berg, abdul Bishop` 208 → 218, `Berg, abdul Prof.` 207 → 228), rare shapes no guard sees. Recorded negative controls: with no title declined, 5 tests fail (the row on both surfaces, H5's example and two P5 examples); with the past-the-second-comma half alone removed, the row `a_bound_join_declines_a_title_past_the_second_comma` (both surfaces) and one P5 example. - ### C1 — the suffix-comma decision - 2026-07 (v2 core, PR #288) — v1 parity: only the second segment decides (v1 parser.py:1318), and ">1 word before the first comma" is v1's guard, which is why "Smith, PhD" keeps the listing form. The strict/lenient knob's blast radius on default vocabulary is exactly the single-letter Roman numerals i and v — the only initial-shaped members of the shipped suffix words — which is what makes rules.md#C1's "V." examples the canonical pair. diff --git a/docs/design/rules.md b/docs/design/rules.md index 69b02771..4cc07004 100644 --- a/docs/design/rules.md +++ b/docs/design/rules.md @@ -724,7 +724,8 @@ P5. Rationale: some given-name words are incomplete alone — "abdul" vocabulary (S2), wherever position will then place it, nor a title word a trailing rule reads as a title: one the trailing title run (H5) takes from the given part after a family comma, - or any title word in a part past the second comma (C2); a bare + read as the comma-less name reads it, or any title word in a part + past the second comma (C2); a bare ambiguous acronym is a name word wherever the peel does not take it; a marker left as a word that a particle join (P2) has already taken travels with that join. A title word standing in the name @@ -799,7 +800,15 @@ P5. Rationale: some given-name words are incomplete alone — "abdul" "Sheik abdul salam" family-first → family="abdul salam" "Sheik abdul salam" family-first → given="" "Sheik abdul salam" family-first-given-last → family="abdul salam" - history: decisions.md#P5 · interacts: S2, M2, H1, H5, P2, P4, P6 · implemented: nameparser/_pipeline/_group.py, nameparser/_pipeline/_post_rules.py + Accepted: the given part after a family comma reads the words + behind a title by its own walk, and where that walk reads them + otherwise than the comma-less name does, the join is declined and + no title is read — a shape-only numeral (VI to X), or V before a + third comma part, stays a name word there, and a title word that + is also a particle is attached by P6 (`Berg, abdul Prof. VI` + reads middle 'Prof. VI'; `Berg, abdul St.` reads family 'St. + Berg'), each as the same name with an ordinary given word reads. + history: decisions.md#P5 · interacts: C2, S2, M2, H1, H5, P2, P4, P6 · implemented: nameparser/_pipeline/_group.py, nameparser/_pipeline/_post_rules.py P6. Rationale: a particle ending the name has nothing to link forward to, so it is not doing a particle's work there. What it @@ -2118,7 +2127,7 @@ C2. Rationale: text beyond the recognized comma parts should be reports on outside a tail: the_chain_reports_the_acronym_it_takes and titled_particle_chain_survives_a_title_that_is_also_a_particle. - history: decisions.md#C1, decisions.md#S2 · interacts: C1, H5, P2, S2, S3 · implemented: nameparser/_pipeline/_segment.py, nameparser/_pipeline/_assign.py, nameparser/_pipeline/_group.py + history: decisions.md#C1, decisions.md#S2 · interacts: C1, H5, P2, P5, S2, S3 · implemented: nameparser/_pipeline/_segment.py, nameparser/_pipeline/_assign.py, nameparser/_pipeline/_group.py ## Name order (O) diff --git a/docs/release_log.rst b/docs/release_log.rst index c4185fd1..3c7b7e1f 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -22,7 +22,7 @@ Release Log - **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) - - **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.``. A bound given name there no longer takes a trailing title into it: ``HumanName("Berg, abdul Prof.")`` gives title ``Prof.``, first ``abdul``, where 1.4.0, 2.2.0 and 2.3.0 gave first ``abdul Prof.`` and 2.0.0 and 2.1.0 middle ``Prof.``. See the #614 entry under ``S2``, the #623 entry under ``H5`` and the #637 entry under ``P5`` in ``docs/design/decisions.md`` (closes #614, #623, #637) + - **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.``. A bound given name behind a family comma no longer takes a trailing title into it: ``HumanName("Berg, abdul Prof.")`` gives title ``Prof.``, first ``abdul``, where 1.4.0, 2.2.0 and 2.3.0 gave first ``abdul Prof.`` and 2.0.0 and 2.1.0 middle ``Prof.``. See the #614 entry under ``S2``, the #623 entry under ``H5`` and the #637 entry under ``P5`` in ``docs/design/decisions.md`` (closes #614, #623, #637) - **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) @@ -44,7 +44,7 @@ Release Log - **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) - - **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.``. A bound given name no longer takes such a title word into it: ``HumanName("John Smith, Jr., abdul Dr. Smith")`` gives title ``Dr.``, suffix ``Jr., abdul Smith``. (#603, closes #629, #636, #640) + - **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.``. A bound given name there no longer takes a title word into it: ``HumanName("John Smith, Jr., abdul Dr. Smith")`` gives title ``Dr.``, suffix ``Jr., abdul Smith``. (#603, closes #629, #636, #640) - **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) diff --git a/nameparser/_pipeline/_group.py b/nameparser/_pipeline/_group.py index f5a5083f..4fff71ec 100644 --- a/nameparser/_pipeline/_group.py +++ b/nameparser/_pipeline/_group.py @@ -131,8 +131,10 @@ class BoundJoin(IntEnum): """v1 _join_bound_first_name's reserve_last, as the three states it actually has. IntEnum: the value IS the number of name pieces assign's peel must leave in the JOINED view for the join to stand - (#425), so the >= comparison below reads unchanged. Post-comma no - peel is run -- the pair alone is that one piece -- and DISABLED + (#425), so the >= comparison below reads unchanged. Post-comma the + reserve runs no peel -- the pair alone is that one piece; the + decline reads H5's trailing run only where a title word follows + the bound word (#637) -- and DISABLED is a mode, never compared: as a threshold 0 would join everything, which is why the block is entered on identity first.""" @@ -1188,9 +1190,12 @@ def chain(tail: int) -> None: # union from making the joined piece a suffix piece. # rules.md#P5: "nor a title word a trailing rule reads as a # title" -- after a family comma one H5's trailing run takes - # from the given part, which the LENIENT join reads no peel - # for and so asks here, only where a title word follows - # ('Berg, abdul Prof.', #637); in a part past the second + # from the given part, read as the comma-less name reads it, + # which the LENIENT join reads no peel for and so asks here, + # only where a title word follows ('Berg, abdul Prof.', + # #637; where the given part's own walk then reads the words + # behind it otherwise, the title stays a name word, which + # rules.md#P5 accepts); in a part past the second # comma any title word, which C2 reads as a title ('John # Smith, Jr., abdul Dr. Smith', #640). A title word standing # in the name is a name word and joins ('abdul Sir Smith @@ -1198,12 +1203,13 @@ def chain(tail: int) -> None: nxt, nxt_tags = pieces[fk + 1], ptags[fk + 1] if (_is_maiden_marker_piece(nxt, tokens) or is_suffix_piece(nxt, nxt_tags, tokens) - or (is_title_piece(nxt, nxt_tags, tokens) + or ((site is ClauseSite.TAIL + or bound_join is BoundJoin.LENIENT) + and is_title_piece(nxt, nxt_tags, tokens) and (site is ClauseSite.TAIL - or (bound_join is BoundJoin.LENIENT - and fk + 1 in tail_reading( - peel_walk(fk, ptags), pieces, ptags, - tokens, one_case)[1])))): + or fk + 1 in tail_reading( + peel_walk(fk, ptags), pieces, ptags, + tokens, one_case)[1]))): pass elif bound_join is BoundJoin.LENIENT: # post-comma the family is fixed and the pair is the diff --git a/tests/v2/cases.py b/tests/v2/cases.py index 6aaeed7a..d35d3530 100644 --- a/tests/v2/cases.py +++ b/tests/v2/cases.py @@ -9816,9 +9816,30 @@ def _check_cjk_shape_purity(self) -> None: notes="a part past the second comma holds no name word, so " "every title word in it is a title (rules.md#C2) and the " "bound join declines one (rules.md#P5, #640), as a " - "family comma's tail always read ('Smith, John, abdul " - "Dr. Smith'); 1.4.0 and 2.3.0 read suffix 'Jr., abdul " - "Dr. Smith'"), + "family comma's tail reads since #603 ('Smith, John, " + "abdul Dr. Smith'); 1.4.0 and 2.3.0 read suffix 'Jr., " + "abdul Dr. Smith'"), + Case("a_bound_join_declines_a_bare_title_past_the_second_comma", + "John Smith, Jr., abdul Sir Smith", + {"title": "Sir", "given": "John", "family": "Smith", + "suffix": "Jr., abdul Smith"}, + ambiguities=("comma-structure",), + classification="fix(#640)", + notes="the row above with a bare title word: C2 reads any title " + "word past the second comma as a title, period-marked or " + "not, so the join declines it too (#643's review: a " + "period-only test passed every other row); 1.4.0 and " + "2.3.0 read suffix 'Jr., abdul Sir Smith'"), + Case("a_bound_join_declines_a_run_of_trailing_titles", + "Berg, abdul Prof. Dr.", + {"title": "Prof. Dr.", "given": "abdul", "family": "Berg"}, + classification="fix(#637)", + notes="after a family comma the join asks H5's whole trailing " + "run, not whether the title is the last word: 'Prof.' " + "stands before another title the run takes as well " + "(#643's review: a last-word test passed every other " + "row); 1.4.0 read first 'abdul Prof.', suffix 'Dr.', and " + "2.3.0 title 'Dr.', first 'abdul Prof.'"), Case("title_run_leading_addresses_over_a_trailing_title", "Sir John Prof.", {"title": "Sir Prof.", "given": "John"}, diff --git a/tests/v2/test_ledger_guards.py b/tests/v2/test_ledger_guards.py index 7d2353fa..0af75de4 100644 --- a/tests/v2/test_ledger_guards.py +++ b/tests/v2/test_ledger_guards.py @@ -3991,8 +3991,8 @@ def _claim(rule: dict) -> _Claim: # 2026-10-06, #613: 452 -> 454; its reach absorbed both C1 examples #613 added to the corpus, 'John Smith, Mr. and Mrs.' and 'Ortega y Gasset, Dr.'. # 2026-10-06, #617: 454 -> 455; its reach absorbed the C1 example 'Smith, Chancellor of the Exchequer MP' #617 added to rules.md. # 2026-10-10, #623: 455 -> 457; its reach absorbed the two rules.md#H5 examples #623 added, 'Smith, John van der Berg Prof.' and 'Smith, Jan van Prof.', both explained by fix(#623). - # 2026-10-11, #636: 457 -> 458; its reach absorbed the rules.md#C2 example 'John Smith, Jr., van der Berg Prof.' #636 added, which at 1.4.0 it explains (suffix and title), as fix(#636) does at 2.x. - # 2026-10-11, #637/#640: 458 -> 461; its reach absorbed the three rules.md#P5 examples #637/#640 added, 'Berg, abdul Prof.', 'Berg, abdul Prof. Ali' and 'John Smith, Jr., abdul Dr. Smith', the first and last of which it explains at 1.4.0, as fix(#637) and fix(#640) do at 2.x. + # 2026-10-11, #636: 457 -> 458; its reach absorbed the rules.md#C2 example 'John Smith, Jr., van der Berg Prof.' #636 added. Reach only: it moves title, outside this rule's fields; the lone post-comma rule explains it at 1.4.0. + # 2026-10-11, #637/#640: 458 -> 461; its reach absorbed the three rules.md#P5 examples #637/#640 added, 'Berg, abdul Prof.', 'Berg, abdul Prof. Ali' and 'John Smith, Jr., abdul Dr. Smith'. Reach only: the moved two move title, outside this rule's fields; the lone post-comma rule explains them at 1.4.0. "fix(comma-precomma-family) pre-comma run reads as family, not given": # 2026-10-01, #564: 423 -> 430, 'John Smith, XYZ', 'Smith, # XYZ', 'García Márquez, MJ', 'García Márquez, MJ PhD', From baa310cedeaf80e01ae8e53d0be794293329b872 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sun, 11 Oct 2026 12:15:36 -0700 Subject: [PATCH 3/3] Fix the review of #643's fix commit: H5's sentence, one frame cost rules.md#H5's Accepted paragraph said the title is read after a family comma, which P5's new Accepted clause now excepts; the frames sentence gains the family-comma bound name with no title behind it. Co-Authored-By: Claude Opus 5.5 --- docs/design/decisions.md | 2 +- docs/design/rules.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/design/decisions.md b/docs/design/decisions.md index e982e2c0..a64337a1 100644 --- a/docs/design/decisions.md +++ b/docs/design/decisions.md @@ -891,7 +891,7 @@ Closes #469, and continues the corpus-tier arc below rather than standing apart Three things the principle does not reach, recorded so nobody applies it there. The post-comma LENIENT path is untouched (AMENDED by #637, below: its decline now reads H5's trailing run where a title word follows the bound word), and NOT for the reason first written here ("its walk has no peel" — it reads a trailing numeral by the lenient last-of-two rule, as the #423 entry says): the family is fixed there and the pair is the given whatever follows, so the principle is not applied, and `Berg, abdul V` keeps given 'abdul V' though the V would be the suffix unjoined — 1.4.0 parity, and exactly the regression #423 measured when a mirror was applied there; P5 now carries it as Accepted. The vocabulary decline is the UNAMBIGUOUS vocabulary: a bare ambiguous acronym the peel does not take is a name word, so `abdul Ma Smith` reads given 'abdul Ma' (1.4.0 read middle 'Ma' — a 2.x difference no corpus witnesses, now an Accepted example). And the principle is about the suffix reading, not the report: `Sheik abdul Ma` reports no SUFFIX_OR_NAME where `Sheik John Ma` does, the #369 precedent — a bound word read as the bound word is not a fork. One more class moves, found by the code review and then measured against 1.4.0: the 2.x reserve had excluded TITLE pieces from its count — an unrecorded deviation from v1, whose join counted suffixes out and nothing else — and the shared peel, following assign, reads a mid-name title word as the name word it is. `Berg, abdul Sir` (given 'abdul', middle 'Sir' since 2.0) and `abdul Smith Sir` (declined since 2.0) read given 'abdul Sir' and given 'abdul Smith', family 'Sir' — 1.4.0's readings, both; `abdul Sir Smith Berg` never stopped reading given 'abdul Sir'. No corpus name has the shapes that move, and no rules example carries one — `abdul Sir Smith Berg`, P5's example of the reading, is byte-identical at every baseline, so the class is pinned in tests alone (the release log says so; the late review of this entry found the sentence here implying otherwise). The same review found the clause had taken `do`, `freiherr` and `st` from P6 — recorded under P6. What the review actually caught was the tag artefact that exclusion had been hiding: the conjunction merge derives a `title` piece tag for a chain such as "Sheikh and Ahmad", merge()'s union handed it to the pair, and assign peeled the pair as a leading title — `abdul Sheikh and Ahmad Bakar Smith` read title 'abdul Sheikh and Ahmad' on 2.0 and 2.1 (v1 read it as the given), and `abdul Sheikh and Ahmad Bakar`, which the exclusion had kept unjoined, would have too once it went. The bound join drops the tag, and the view drops it the same way — the pair is a given name whatever tag the word carried. One more class the comparison moves, found by the code review's classification of every residual master-to-branch diff (12 of 17,640 constructed names — a fourteen-word pool, bare and behind `Sir`, tails of two and three, 6 × (14² + 14³); the review did not record the pool — one shape; 360 of 360 over {Sir, Sheikh, Dame, Sheik, King, Father} × {abdul, abd, abu, umm} × {J., V, X, A., I} × {V, X, I}): behind a given-name title, a numeral after an initial-shaped piece. `Sir abdul J. V` read given 'abdul J.', suffix 'V' since #369 — the fork suppressed unjoined, fired joined, and the licence's threshold of one let the join through — and the comparison declines it: given 'abdul', middle 'J.', family 'V', exactly as `Sir John J. V`. Pinned at both levels; it was the only shape a looser comparison let through under mutation at the test review. #424 — P2's chain and M2's walk taking a trailing numeral — is the remaining asker of the same question and now has `_peel_trailing` to ask it of. - 2026-10-11 (Derek), #637 and #640 — THE BOUND JOIN DECLINES A TITLE WORD A TRAILING RULE READS AS A TITLE. P5 joins a bound given-name word to the word after it, and declined a maiden marker and a suffix piece; a title word it took, so where a trailing rule would have read that word as a title the joined piece was no title word to read. After a family comma the join reads no peel (the LENIENT reserve, #423), so `Berg, abdul Prof.` read given 'abdul Prof.' where `Berg, John Prof.` reads title 'Prof.' (#637, the gap the 2026-09-09 A1 entry recorded); in a part past the second comma after a suffix comma, where the join runs STRICT, `John Smith, Jr., abdul Dr. Smith` read suffix 'Jr., abdul Dr. Smith' where `John Smith, Jr., Berg Dr. Smith` reads title 'Dr.' (#640). Now the join declines a title word a trailing rule reads as a title: after a family comma, one H5's trailing run takes from the given part (`tail_reading`, asked only when a title word follows the bound word), and in a part past the second comma any title word, which C2 reads as a title. `Berg, abdul Prof.` → title 'Prof.', given 'abdul'; `John Smith, Jr., abdul Dr. Smith` → title 'Dr.'. DECIDED (Derek): this narrower clause (a′) rather than declining every title word (a). P5 states that a title word standing in the name is a name word and joins like one (H3), and (a) undid it: it failed six tests, a P5 rules example and the two tests named for that clause among them, and over the bound-given grid below moved 4,044 parses, 3,000 of them a join split with no title read (`(Bob), Abu Bishop` → given 'Abu', middle 'Bishop') beside the 1,044 that (a′) moves (six tests measured against 4455962f's suite). (a′) leaves every join on a title word inside the name standing -- `abdul Sir Smith Berg`, `Berg, abdul Prof. Ali`, `Smith, abdul Bishop`. - MEASURED 2026-10-11 against 4455962f (#639's merge), py3.11, `nameparser.__file__` asserted on each side. A bound-given grid -- `{head} {bound} {next} {rest}` for head in (none), `Dr.`, `Sir`, `Smith,`, `van der Berg,`, `,`, `(Bob),`, `Smith, John,`, `John Smith, Jr.,`, `John Smith, PhD,`, `John`, `Mr. and Mrs.`; bound in `abdul`, `Abu`, `abd al`; next in `Dr.`, `Prof.`, `Prof`, `Bishop`, `King`, `King.`, `Sir`, `St`, `Freiherr`, `Salam`, `Rahman`, `Lt.Gov.`, `Mr.`, `van`, `MA`, `J.`; rest in (none), `Smith`, `Ali Smith`, `Dr.`, `Smith Dr.`, `Jr.`, `MA`, `Smith, Jr.`, `Prof. Smith` -- 5,184 texts under the three orders, comparing the seven fields and every report: 1,044 parses move, every one a title word into the title field and no report moved alone -- 660 in a part past the second comma (`John Smith, Jr., Abu Bishop Ali Smith` → title 'Bishop', as C2 reads `John Smith, Jr., Bishop`), the rest after a family comma or an empty head (`(Bob), Abu King.` → title 'King.', given 'Abu'). #623's given-part grid (decisions.md#H5): 280 -- 260 the same class (`(Bob), abdul Dr.` → title 'Dr.', family 'abdul', H1's move behind a title), and 20 that split the join with no title read (`Smith, abdul Prof. V, Jr.` → given 'abdul', middle 'Prof. V'), the class below. Every corpus name and case text at 4455962f: only `Berg, abdul Prof.`, the row that had pinned the gap. The S2 stress grid, #624's title-particle grid and #625's particle grid: 0. The five gates exit 0, with `fix(#637)` and `fix(#640)` rules in each 2.x ledger; at 1.4.0 the broad `fix(comma-family) lone post-comma piece routes to suffix/title` rule explains both new examples. Frames: the reference rows do not move; a comma-less name with a bound given word pays nothing, the site asked before the title test (`abdul salam ahmed salem` 241), and one with a title word behind it after a family comma about ten to twenty for the H5 read (`Berg, abdul Bishop` 208 → 218, `Berg, abdul Prof.` 207 → 228), rare shapes no guard sees. Recorded negative controls (with the review's rows): with no title declined, 11 tests fail (the four #637/#640 rows on both surfaces, H5's example and two P5 examples); with the past-the-second-comma half alone removed, 5 (its two rows on both surfaces and one P5 example). + MEASURED 2026-10-11 against 4455962f (#639's merge), py3.11, `nameparser.__file__` asserted on each side. A bound-given grid -- `{head} {bound} {next} {rest}` for head in (none), `Dr.`, `Sir`, `Smith,`, `van der Berg,`, `,`, `(Bob),`, `Smith, John,`, `John Smith, Jr.,`, `John Smith, PhD,`, `John`, `Mr. and Mrs.`; bound in `abdul`, `Abu`, `abd al`; next in `Dr.`, `Prof.`, `Prof`, `Bishop`, `King`, `King.`, `Sir`, `St`, `Freiherr`, `Salam`, `Rahman`, `Lt.Gov.`, `Mr.`, `van`, `MA`, `J.`; rest in (none), `Smith`, `Ali Smith`, `Dr.`, `Smith Dr.`, `Jr.`, `MA`, `Smith, Jr.`, `Prof. Smith` -- 5,184 texts under the three orders, comparing the seven fields and every report: 1,044 parses move, every one a title word into the title field and no report moved alone -- 660 in a part past the second comma (`John Smith, Jr., Abu Bishop Ali Smith` → title 'Bishop', as C2 reads `John Smith, Jr., Bishop`), the rest after a family comma or an empty head (`(Bob), Abu King.` → title 'King.', given 'Abu'). #623's given-part grid (decisions.md#H5): 280 -- 260 the same class (`(Bob), abdul Dr.` → title 'Dr.', family 'abdul', H1's move behind a title), and 20 that split the join with no title read (`Smith, abdul Prof. V, Jr.` → given 'abdul', middle 'Prof. V'), the class below. Every corpus name and case text at 4455962f: only `Berg, abdul Prof.`, the row that had pinned the gap. The S2 stress grid, #624's title-particle grid and #625's particle grid: 0. The five gates exit 0, with `fix(#637)` and `fix(#640)` rules in each 2.x ledger; at 1.4.0 the broad `fix(comma-family) lone post-comma piece routes to suffix/title` rule explains both new examples. Frames: the reference rows do not move; a comma-less name with a bound given word pays nothing, the site asked before the title test (`abdul salam ahmed salem` 241), a family-comma one with no title behind it one for the title test (`Berg, abdul salam` 208 → 209), and one with a title word behind it after a family comma about ten to twenty for the H5 read (`Berg, abdul Bishop` 208 → 218, `Berg, abdul Prof.` 207 → 228), rare shapes no guard sees. Recorded negative controls (with the review's rows): with no title declined, 11 tests fail (the four #637/#640 rows on both surfaces, H5's example and two P5 examples); with the past-the-second-comma half alone removed, 5 (its two rows on both surfaces and one P5 example). THE REVIEW (2026-10-11). The decline after a family comma asks H5's run as the comma-less name reads it (`tail_reading`), and the given part's own walk reads some words behind a title otherwise, the disagreement #623's entry (decisions.md#H5) records for the chain's bound: there the join is declined and no title is read. A shape-only numeral VI to X, or V before a third comma part, which the walk keeps a name word (`Smith, abdul Prof. VI` → given 'abdul', middle 'Prof. VI', where master and 1.4.0 read given 'abdul Prof.', middle 'VI'); a period-marked title that is also a particle, which P6 then attaches with its report (`Smith, abdul St.` → family 'St. Smith', particle-or-given); and a bound word that is also a particle before one (`Smith, Abu St.` → title 'St.', where `Smith, Zed St.` reads family 'St. Smith'). Every such shape is garbage; the first two read as the same name with an ordinary given word does. Recorded, not narrowed, as #623's disagreement was (Derek, #623), and rules.md#P5 states it as an Accepted clause; the review's sweep found nothing reading worse than master on a real name and no comma-less name moving. It also found two mechanisms no test held, now rows: the bare-title half past the second comma (`John Smith, Jr., abdul Sir Smith`) and the whole trailing run after a family comma rather than the last word (`Berg, abdul Prof. Dr.`). Excluded (BOUND_GIVEN_NAMES): diff --git a/docs/design/rules.md b/docs/design/rules.md index 4cc07004..734ada9b 100644 --- a/docs/design/rules.md +++ b/docs/design/rules.md @@ -354,8 +354,9 @@ H5. Rationale: a word abbreviated with a period at the END of a name comma-less writing, which is where the chain runs before the reserve is read. After a family comma the reserve reads no peel, but the join declines a trailing title word the chain takes (P5), - so the title is read there too, as the segment written with an - ordinary given name reads it. + so the title is read there too, save the shapes P5's Accepted + clause records, as the segment written with an ordinary given + name reads it. "Berg, abdul Prof." → title="Prof." 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