Skip to content

Commit ae6dc61

Browse files
derek73claude
andcommitted
fix(bound-given): decline the join rather than absorb a maiden marker
Review found the reserve fix necessary but not sufficient, and two prose claims measurably false. Excluding the departing words is not enough on its own. Where a suffix sits INSIDE the name the marker walk stops early, so the excluded span is short, enough words survive it to clear the reserve, and the join fires and takes the MARKER -- after which nothing leaves at all and the count that authorised the join was reasoning about a name that never came to be. `abd née Jones Jr Smith Berg` read given 'abd née'. The join now declines outright when the piece it would absorb is a marker, which is what P5 meant all along: it joins the bound word to a name word, and a marker is not one. Tested against the marker directly rather than through the span, because the two differ exactly where it matters -- a marker with nothing but a suffix after it has no span at all, yet the join would still have absorbed it (`Berg, abdul née PhD` read given 'abdul née'). That closes the P5 half of #412 for real; the first cut only narrowed it while claiming it closed. The docstring's "the count and the removal cannot disagree" was the same overstatement this session already shipped once. Sharing the span helper settles what a maiden span IS; it does not settle what leaves, because the two calls run at different times on different piece lists and the join between them can invalidate the span. Forbidding the join to absorb a marker is what closes that -- the sharing only rules out disagreeing about the span itself. The pre-existence claim was backwards. `abd Berg née Jones` reads CORRECTLY at 2.1.0; `abd` acquired the defect only when #400 made it a bound given-name word this cycle, so for that spelling this fixes a regression introduced earlier in the same release. The pre-existing witness is `abdul`/`abdel`/`abdal`, which read the defect at 2.0.0 and 2.1.0 alike. The issue body carries the same error and needs it too. Six more rows, each closing a measured gap: - `Abd Berg née Mary Jones`. Capping the excluded span at two pieces reproduced #411 exactly with the whole suite green -- every existing row had a one-word maiden name, so none could tell the span from the marker plus one. A particle-led maiden name does not serve; P2 makes it a single piece. - The two shapes the new guard fixes, and `Berg, abd née Jones`, where a declining join after a family comma leaves S2's suffix reading and the name has no given name at all -- as `Berg, abd` alone always parsed. It reads alarmingly and was in no bullet and no row. - The marker immediately after the bound word, and the Arabic script. Plus a piece-level pin that the join never merges a marker, and the bound-given shapes added to the #409 chain invariant's inputs -- that invariant is keyed on the particle chain, and the distinction is now exercised rather than assumed. `Berg, abd née Jones` arrived UNEXPLAINED at 1.4.0 on four fields, the subset test correctly rejecting both nearby rules, and is read once here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 252dbea commit ae6dc61

11 files changed

Lines changed: 247 additions & 50 deletions

File tree

docs/design/decisions.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,15 @@ Open: [#380](https://github.com/derek73/python-nameparser/issues/380) covers "Be
117117

118118
Two things fell out rather than being decided separately: the same issue's open question about the leftover given slot (the marker used to compete for it, giving `de la Cruz née Vega` given "née" under a family-first order; consumed and dropped, it never reaches the placement — though a marker the consumer declines still does, which is M2's "just a word" reading and not a leak), and the wholly-particle family a stop can leave, which #404's R2 reading already covers — "Jane de la née Jones" reports family "de la" with a non-empty base. A 305,364-parse sweep at review found no empty-base or partition violation either side of the change.
119119

120-
Two limits stay, now recorded in M2 rather than left implicit: the bound reaches only a marker standing as a word of its own, so P3's connective join and P5's bound-given join each still absorb a marker before the bound can see it — the deleted Accepted clause covered a class of which this fixed one instance and left two. Tests carry the whole verification: of the marker-bearing differential corpus names none has a particle ahead of the marker, so the gate is blind to this class and its clean exit says nothing about the change. A synthetic cross-script row was written to reach the CJK corpus builder — the only route a case row has into any corpus — and then removed: it used that corpus's provenance as a back door, and it pinned nothing the Latin rows did not. The honest route is [#413](https://github.com/derek73/python-nameparser/issues/413), where the harvester's pattern turns out to miss every name written in backticks, which is how this tracker writes them. `Ursula von der Leyen geb. Albrecht` sat in #399's own title the entire time.
120+
Two limits stay, now recorded in M2 rather than left implicit: the bound reaches only a marker standing as a word of its own, so P3's connective join and P5's bound-given join each still absorb a marker before the bound can see it — the deleted Accepted clause covered a class of which this fixed one instance and left two. (P5's was closed by #411 on 2026-08-21, leaving P3's.) Tests carry the whole verification: of the marker-bearing differential corpus names none has a particle ahead of the marker, so the gate is blind to this class and its clean exit says nothing about the change. A synthetic cross-script row was written to reach the CJK corpus builder — the only route a case row has into any corpus — and then removed: it used that corpus's provenance as a back door, and it pinned nothing the Latin rows did not. The honest route is [#413](https://github.com/derek73/python-nameparser/issues/413), where the harvester's pattern turns out to miss every name written in backticks, which is how this tracker writes them. `Ursula von der Leyen geb. Albrecht` sat in #399's own title the entire time.
121121

122122
Open (M2):
123123
[#317](https://github.com/derek73/python-nameparser/issues/317)
124124
the fullwidth-colon marker (旧姓:佐藤 arrives as one word; the head-peel question).
125125
[#412](https://github.com/derek73/python-nameparser/issues/412)
126-
the two surviving join-swallows (P3's connective join and P5's bound-given join each absorb a marker before M2's bound can see it) — where the reorder set aside above would be re-weighed.
126+
the surviving join-swallow — P3's connective join absorbs a marker before M2's bound can see it. P5's bound-given join did too until #411 stopped it joining a marker at all (2026-08-21), so this narrowed from two instances to one; it is where the reorder set aside above would be re-weighed.
127127
[#410](https://github.com/derek73/python-nameparser/issues/410)
128-
a maiden name makes H1 decline, so a title-plus-surname name reports no family; [#411](https://github.com/derek73/python-nameparser/issues/411) the same empty family from P5's reserve counting words the maiden name takes away. Both pre-existing, both widened by the stop.
128+
a maiden name makes H1 decline, so a title-plus-surname name reports no family — pre-existing, and widened by the stop. (#411, the same empty family from P5's reserve, closed 2026-08-21.)
129129

130130
### N3 — the lone-word nickname rule
131131

@@ -330,6 +330,16 @@ Excluded (SUFFIX_ACRONYMS / SUFFIX_WORDS — the esq dual membership, deliberate
330330
- 2026-06-30 (first-name-prefix-join design; v1-era, carried into the v2 port) — the join is vocabulary-driven and deliberately tiny.
331331

332332
- 2026-08-18 — `abd` ADDED, and the reserve count corrected to let it work. It is the transliteration for the spellings that write the article as its own word ("Abd Allah", "Abd al-Rahman"), which abdul/abdel/abdal do not match; the Arabic-script عبد has covered the same word since #269, so only the Latin side was short. Not a C-ii collision, though it looks like one: C-ii settles a PRECEDENCE by frequency where neither reading is a name, and here one reading is a name and neither had to be given up. It is C-i's positional qualifier (2026-08-17) applied to a bound-given word — the naming use is LEADING, the credential use trailing, which is the `de` row of that table ('a TRAILING surname — no clash') rather than the `vd` row. `abd` is also the postnominal ABD ("All But Dissertation") in SUFFIX_ACRONYMS, and all three credential spellings still parse ("Jane Smith ABD", "Jane Smith, ABD", "Jane Smith A.B.D.").
333+
334+
- 2026-08-21 #411 (PR #416) — the reserve counts the name the join will actually leave behind, and the join never absorbs a marker. P5 has always said the join "needs a name word to spare, so two name words alone do not join"; the count was taken while the maiden marker and the maiden name were still pieces, because group's marker pass runs later in the same stage. `abdul Berg née Jones` counted four words, joined, and left no family name at all — given 'abdul Berg', family ''. That spelling is the pre-existing witness: it reads the same way at 2.0.0 and 2.1.0. `abd` does NOT — it reads correctly at 2.1.0 and acquired the defect only when #400 made it a bound given-name word this cycle, so for that spelling this is a regression #400 introduced and this closes. #399's chain stop widened the class again by putting the particle spellings in front of the reserve for the first time.
335+
336+
Excluding the departing words is not sufficient on its own, and review is what established that. Where the maiden walk stops at an inner suffix the excluded span is short, enough words survive it to clear the threshold, and the join fires and takes the MARKER — after which nothing leaves and the count that authorised the join was reasoning about a name that never came to be. So the join now declines outright when the piece it would absorb is a marker, which is what P5 meant all along: it joins the bound word to a name word, and a marker is not one. Tested directly rather than through the span, because the two differ exactly where it matters — a marker with nothing but a suffix after it has no span at all, yet the join would still have absorbed it (`Berg, abdul née PhD` read given 'abdul née').
337+
338+
What sharing `_maiden_span` between the count and the marker pass buys is one definition of what a maiden span IS. It does not make the two agree about what leaves: they run at different times on different piece lists, and the join between them could invalidate the span the count was taken against. Forbidding the join to absorb a marker is what closes that, not the sharing — the sharing only keeps the two from disagreeing about the span itself.
339+
340+
Two consequences. It closes the P5 half of the join-swallow #412 records, so that issue narrows to P3's connective join. And where the bound word is also suffix vocabulary, a declining join after a family comma leaves the S2 suffix reading and the name has no given name at all — `Berg, abd née Jones` reads family 'Berg', suffix 'abd', maiden 'Jones', which is exactly how `Berg, abd` alone has always parsed. `abd` is the only shipped word in both sets.
341+
342+
No corpus name produces a #411-attributable diff — every baseline already reads these names the way the fix does — so the ledger rule records the side effect on `van der Berg, abdul née Jones` rather than the defect. That name became an M2 example in the #399 fix and reached the corpus when #414's rules-doc corpus harvested it the following day.
333343
What made it look impossible: adding the word alone changed nothing on the common three-word shape. The reserve count in the join gate excluded suffix-vocabulary pieces, and `abd` IS one, so its own piece did not count toward the three it needed — the rule declined silently. Counting the claimed piece as a name piece fixes it, and is right on its own terms: the reserve asks whether enough OTHER words are left to spare. Measured, the vocabulary-only alternatives both cost something the count fix does not — moving `abd` into SUFFIX_ACRONYMS_AMBIGUOUS loses "Jane Smith, ABD", and dropping it from SUFFIX_ACRONYMS loses the postnominal entirely.
334344
Two limits, both measured and both accepted. The three-token "abd al rahman" still does not join — `al` is a particle and chains forward, which is the half of the original exclusion that survives. And position decides at the two ENDS only: in the given slot of a family-comma name the credential still wins, so "Smith, Abd" reports suffix 'Abd' with no given name where "Smith, Abdul" reports the given name, and no ambiguity is emitted for it.
335345
Same shape as #397 (Catalan `i`, where dual membership shrinks the rootname count P3's carve-out tests), and the second instance the SAME DAY, #397 having been filed two and a half hours earlier. The general question — whether a word's membership in one set should suppress it from a count gating a rule keyed on another — is open there.
@@ -380,7 +390,6 @@ Excluded (MAIDEN_MARKERS, per nameparser/config/maiden_markers.py):
380390
- "born" — never shipped: a release-log drafting invention, caught by the 2.0 milestone audit and corrected (5ccf9f3). Recorded so nobody "restores" it; if ever proposed for real, Max Born is the counterexample to analyze.
381391
- Scandinavian "f." — collides with the initial F.; only the full participles (født/fødd/född) are safe. Czech masculine "rozený" awaits the same vetting.
382392

383-
- 2026-08-19 #411 (PR #416) — the reserve counts the name the join will actually leave behind, not the pieces present when it is computed. P5 has always said the join "needs a name word to spare, so two name words alone do not join"; the implementation counted while the maiden marker and the maiden name were still pieces, because group's marker pass runs later in the same stage. So `Abd Berg née Jones` counted four words, joined, and left no family name at all — given 'Abd Berg', family ''. The words a marker will take are now excluded from the count, through the same span helper the marker pass itself uses: one definition of what leaves, so the count and the removal cannot disagree, which is the shape #399 settled for the marker predicate. Pre-existing (the marker-less spelling behaved this way at 2.1.0 too) and widened by #399's chain stop, which put the particle spellings in front of the reserve for the first time. Two things fell out. It closes one of the two join-swallows M2 recorded as accepted and #412 tracks: with the reserve counted correctly the join declines on `van der Berg, abdul née Jones`, so the marker is never merged into 'abdul née' and M2's bound sees a lone marker piece — maiden 'Jones' where the name previously read given 'abdul née'. P3's connective join is the one that survives, and #412 narrows to it. And the corpus name that shows it was only there to be shown because the rules-doc corpus (#414) had added it a day earlier as an M2 example; #411 itself has no corpus name, which is why the ledger rule records the side effect rather than the defect.
384393

385394
### C1 — the suffix-comma decision
386395

docs/design/rules.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,17 +236,21 @@ P5. Rationale: some given-name words are incomplete alone — "abdul"
236236
is BOTH bound-given and suffix vocabulary, position decides and
237237
both readings survive: leading, it is the bound word; trailing,
238238
it is the suffix (S2). In the given slot after a family comma the
239-
suffix reading wins. Words a maiden marker will take are not
240-
among the words to spare: they leave the name, so counting them
241-
asks the question about a name that will not exist (M2).
239+
suffix reading wins. The marker and the words it will take are
240+
not among the words to spare: they leave the name, so counting
241+
them asks the question about a name that will not exist. The join
242+
never absorbs the marker itself — a marker is not a name word
243+
(M2).
242244
"abdul salam ahmed salem" → given="abdul salam"
243245
"abd Allah Smith" → given="abd Allah"
244246
"Salam, abd Allah" → given="abd Allah"
245247
"abd Allah" → given="abd"
246248
"Smith, Abd" → suffix="Abd"
247249
"mohamad ali smith" → given="mohamad" · boundary
248250
"abd Berg née Jones" → family="Berg"
249-
"abd Allah Smith née Jones" → given="abd Allah"
251+
"abd Allah Smith née Jones" → given="abd Allah"
252+
"abd née Jones" → given="abd"
253+
"Berg, abd née Jones" → suffix="abd"
250254
history: decisions.md#P5 · interacts: S2, M2 · implemented: nameparser/_pipeline/_group.py, nameparser/_pipeline/_post_rules.py
251255

252256
P6. Rationale: a particle ending the name has nothing to link
@@ -456,7 +460,8 @@ M2. Rationale: a maiden marker announces that what follows it is the
456460
text, not a marker; and the bound reaches only a marker standing
457461
as a word of its own, so the connective join (P3) still absorbs a
458462
marker before the bound can see it; #412 tracks whether it should
459-
reach it.
463+
reach it. The bound-given join (P5) no longer does: it declines
464+
rather than absorb a marker.
460465
"Jane Smith, née Jones" → maiden=""
461466
"Jane van der Berg née y Jones" → maiden=""
462467
"van der Berg, abdul née Jones" → maiden="Jones"

docs/release_log.rst

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

4747
- Move ``mc`` and ``ste`` into the never-given half of the particle vocabulary, and add the Spanish and Portuguese articles ``los``, ``las`` and ``das``, which were absent from it entirely. A never-given particle opening a name folds into the family (``rules.md#P1``) instead of being read as a given name, so ``"Mc Donald"`` was first ``Mc``, last ``Donald`` and is now last ``Mc Donald``; ``"Ste Marie"``, ``"Los Santos"``, ``"Las Casas"`` and ``"Das Silva"`` move the same way, and ``"Mc Donald Smith"`` goes from first ``Mc``, middle ``Donald``, last ``Smith`` to last ``Mc Donald Smith``. The ``PARTICLE_OR_GIVEN`` ambiguity goes with it for ``mc`` and ``ste``, the two that were already in the vocabulary: once the word can never be a given name there is no fork left to report. ``los``, ``las`` and ``das`` never reported one, having not been particles at all. ``mc`` and ``ste`` were already particles and only changed halves, so names where they are not leading are untouched -- ``"Jack Mc Donald"`` already read last ``Mc Donald`` and is byte-identical. ``los``, ``las`` and ``das`` were not particles at all, so for those three the ordinary particle join fires from a non-leading position too: ``"Maria das Neves"`` was first ``Maria``, middle ``das``, last ``Neves`` and is now last ``das Neves``. Never-given membership goes from 28 words to 33. No differential corpus name moved when this landed; ``"Mc Donald"`` entered the corpora afterwards with ``corpus_rules.jsonl`` and is now the one name that pins it (closes #360)
4848

49-
- Fix a bound given-name join leaving no family name when the name also carries a maiden clause. ``"abd Berg née Jones"`` read given ``abd Berg`` with an EMPTY family, where ``"abd Berg"`` alone correctly reads given ``abd``, family ``Berg``. The join reserves a name word so it always leaves a family name behind -- two name words alone do not join -- but the reserve was counted while the maiden marker and the maiden name were still part of the name, and the pass that removes them runs afterwards. Four words counted, the join fired, and when the two departed nothing was left for the family. The words a marker will take no longer count toward the reserve, so those names read given ``abd``, family ``Berg``, maiden ``Jones``, and ``"Abd van der Berg née Jones"`` reads family ``van der Berg`` where it had none. A name with a word to spare is untouched: ``"abd Allah Smith née Jones"`` still joins, giving given ``abd Allah``, family ``Smith``, maiden ``Jones``. The same counting fix closes one of the two cases where a join swallowed a marker before the maiden rule could see it: ``"van der Berg, abdul née Jones"`` read given ``abdul née``, middle ``Jones`` and now reads given ``abdul``, family ``van der Berg``, maiden ``Jones``. The connective join is the remaining case, tracked at `#412 <https://github.com/derek73/python-nameparser/issues/412>`_. This shape is pre-existing rather than new -- ``"abd Berg née Jones"`` behaved this way at 2.1.0 too -- but the particle spellings reached it for the first time through the #399 chain stop earlier in this release (closes #411)
49+
- Fix a bound given-name join leaving no family name when the name also carries a maiden clause, and stop the join absorbing the marker itself. ``"abdul Berg née Jones"`` read given ``abdul Berg`` with an EMPTY family, where ``"abdul Berg"`` alone correctly reads given ``abdul``, family ``Berg``. The join reserves a name word so it always leaves a family name behind -- two name words alone do not join -- but the reserve was counted while the maiden marker and the maiden name were still part of the name, and the pass that removes them runs afterwards. Four words counted, the join fired, and when the two departed nothing was left for the family. The words a marker will take no longer count toward the reserve, so that name reads given ``abdul``, family ``Berg``, maiden ``Jones``. Excluding them is not sufficient on its own: where a suffix sits INSIDE the name the marker walk stops early, enough words survive to clear the reserve, and the join fired and took the marker -- ``"abd née Jones Jr Smith Berg"`` read given ``abd née``. The join now declines outright when the piece it would absorb is a marker, which is what the rule meant all along: it joins the bound word to a name word, and a marker is not one. A name with a word to spare is untouched -- ``"abd Allah Smith née Jones"`` still joins, giving given ``abd Allah``, family ``Smith``, maiden ``Jones``. This is pre-existing for the spellings that were already vocabulary: ``abdul``, ``abdel`` and ``abdal`` read this way at 2.0.0 and 2.1.0 alike. ``abd`` is different -- it reads CORRECTLY at 2.1.0 and acquired the defect only when this same release made it a bound given-name word, so for that spelling this fixes a regression introduced earlier in the cycle; and the particle spellings reached the reserve for the first time through the maiden-marker chain stop above. Two consequences worth knowing. The bound-given join no longer swallows a maiden marker at all, so ``"van der Berg, abdul née Jones"`` reads given ``abdul``, family ``van der Berg``, maiden ``Jones`` where it read given ``abdul née``, middle ``Jones``; the connective join is the remaining case, tracked at `#412 <https://github.com/derek73/python-nameparser/issues/412>`_. And where the bound word is ALSO suffix vocabulary, a declining join after a family comma leaves the post-nominal reading and the name has no given name: ``"Berg, abd née Jones"`` reads family ``Berg``, suffix ``abd``, maiden ``Jones``, matching how ``"Berg, abd"`` alone has always parsed. ``abd`` is the only shipped word in both sets (closes #411)
5050

5151
**Deprecations**
5252

0 commit comments

Comments
 (0)