Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/design/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,21 @@ Open: [#380](https://github.com/derek73/python-nameparser/issues/380) covers "Be
Measured over the 1094-name differential corpus: P6's attachment fires on 9 names, of which 5 take the ambiguous arm (`Beethoven, Ludwig van`, `Berg, Jan van der`, `Nguyen, Thi Van`, `Vega, Juan de la`, `der, y van`), 2 the suffix arm (`Berg, Jan vd`, `Berg, abdul vd`) and 2 stay silent (`Jong, Anke de`, `Jong, Piet de`). Seven names gain an ambiguity kind and no role field moves that was not already moving. Read that seven as evidence about the corpus rather than about the blast radius, the caution the 2026-08-16 entry above already gives for this rule: 304 of the 1094 names carry a comma and only 13 end in a particle, 11 of those being rows the #379/#380 arc added itself (this change added four corpus names and not one of them carries a comma, so the 11 is unchanged by it). The reach is a SHAPE — every family-comma listing ending in an ambiguous particle, which is to say ordinary Dutch, Flemish and unaccented Vietnamese listings — and a caller holding such records sees a report on all of them, not on seven. The LEDGER shape that follows from that is not the one the plan predicted, and the difference is worth recording because it will recur for any report added to a rule whose fields already move. A new `fields = ["_ambiguities"]` rule explains none of the seven: `classify` matches on a rule's `fields` being a SUPERSET of the whole diff, and against 2.0.0 and 2.1.0 these names diff in `middle`/`family` (or `family`/`suffix`) from #379 and #380 as well. So the repair is to WIDEN those two existing rules to `{middle, family, _ambiguities}` and `{family, suffix, _ambiguities}` in both ledgers, not to add a rule. Each widened rule still passes the #452 equality check, since the union over the names it explains is exactly what it declares — the two silent `Jong` names contribute `{middle, family}` and the five ambiguous ones contribute `_ambiguities` on top. 1.4.0 needs nothing at all, having no ambiguity surface to diff. Gate before and after: byte-identical output at all three baselines apart from the provenance header — 1094 names, 229/194/102 intentional, 0 unexplained, and no per-heading count moved. The seven names were already in the diff; only their field sets grew.
The most visible consequence, and it is not the Vietnamese one: `Beethoven, Ludwig van` — the textbook-correct Dutch listing, read exactly right — now carries a report. That is honest rather than regrettable. The parser cannot separate it from `Nguyen, Thi Van`; the two are the same string shape over the same vocabulary, and a report on one is a report on the other. A caller who wants only the doubtful cases has nothing here to filter on, because there is nothing to filter on in the input either.

- 2026-08-30 #365 (the no-comma site) — a declared family-first order names the family, so the attachment fires without a comma too. This DECIDES the parenthesis the 2026-08-16 Scope bullet above left open ("arguably a missing comma under a declared family-first order") and narrows that bullet to the default order, which is the only place nothing names the family. The same Dutch listing is written both ways and now reads alike: `Jong, Anke de` and `Jong Anke de` under a family-first order both give family 'de Jong'.
CREDIT WHERE THE DESIGN CAME FROM, since a first draft of this entry claimed it for the PR. The issue proposed adding MIDDLE as a third P1 site and its revision comment proposed dropping the `FAMILY_FIRST_GIVEN_LAST` fold, and those two ARE both worse — the first loses `Garcia` from `Mesnil Garcia de` because a particle trailed it, the second reports `de` AS the given name, which P1 exists to prevent. But the same comment records a third reading and it is the one that shipped, mechanism included: family-first as an implied family comma, with the note that today's fold "folds while preserving input order" and that `FOLDED_TAG` in `_types._text_for` "already performs exactly that reorder". What this PR added is the guards, not the idea.
NEVER-GIVEN ONLY, where the comma path takes ambiguous particles too, and P6's own Accepted clause is what decides it: the comma-less `FAMILY_FIRST_GIVEN_LAST` form is the ONE format that reads the unaccented Vietnamese spelling correctly, and an ambiguous-inclusive attachment takes that format too — measured, `Nguyen Thi Van` under that order would give family 'Van Nguyen', given 'Thi', where P6's example pins given 'Van'. The words-to-spare guard does not save it (a given word does remain), so the vocabulary line is the only one that holds. The cost is the Dutch flagship word itself: `van` is ambiguous, so `Beethoven Ludwig van` keeps middle 'van' where `Beethoven, Ludwig van` gives family 'van Beethoven'. The two forms agree for the never-given half of the tussenvoegsel set and not for the rest, and the rule says so.
THREE CONDITIONS, EACH A DEFECT BEFORE IT WAS A CONDITION. All three were found by the review round, none by the corpus, and none by the suite.
(i) A NAME WORD MUST REMAIN BESIDES THE FAMILY — P6's own words-to-spare test, in the shape a positional read needs it. Without it the no-comma site was WIDER than the comma site it claims to narrow: `Jong de` attached and gave family 'de Jong' where `Jong, de` declines and gives 'Jong de', so two writings of one one-given-word listing disagreed about word order. It would also have half-decided the no-given-word case that the Open note below deliberately leaves open. With it, the two forms agree row for row, and `Mesnil de` stays P1's fold — which is why P1's example line for it is unchanged by this PR.
(ii) THE FAMILY MUST HAVE A BASE OF ITS OWN, not merely a name word somewhere ahead. The piece test passed on `van Berg Jan de` under `FAMILY_FIRST` and the name lost its given name outright — 'Berg' on master, empty after, family 'de van Berg Jan' — because the leftover is partitioned into UNITS and `_units` chains a leading particle over every word after it, so `[van][Berg][Jan]` collapses to ONE unit and `_name_positions` gives it FAMILY. 288 of a 45,648-parse battery lost a given name that way. P1 masks it for every never-given opening (its lead site fires afterward and re-runs its own narrower reach), which is why every fixture written for this rule — all opening with `de` — missed it.
(iii) NO SUFFIX WORD MAY BE STRANDED AT THE END. assign leaves a trailing `Jr.` in a name position when a particle follows it; take the particle away and the leftover is not the same name one word shorter, because the re-layout does not re-run assign's trailing peel. `Berg Jan Jr. de` reported given 'Jr.' under `FAMILY_FIRST_GIVEN_LAST`, and every battery name where the two family-first orders agreed before and disagreed after was this shape — the rule re-opening the divergence it exists to close. The rule DECLINES rather than re-running the peel: mirroring assign by hand is the failure this project has on record from #424/#425, and a boundary is cheaper than a second copy of its walk.
NO STRUCTURE TEST, which was the fourth defect and the opposite kind — the rule missing an instance of its own bug. `state.order is not None` already excludes every comma that named a family, since assign records no order there; testing `structure is not FAMILY_COMMA` as well excluded ONLY the comma that fixed nothing (segment 1 holding no name word, assign reading segment 0 positionally, #296), where #365's symptom survived verbatim: `Mesnil Garcia de, Dr.` stranded `de` as a middle under `FAMILY_FIRST` and swallowed the given slot under `FAMILY_FIRST_GIVEN_LAST`. Segment 0 is this site's for the same reason, which is `_leading_name_piece`'s rule read at `order is not None`.
THE TWO SITES ARE MUTUALLY EXCLUSIVE, and that retired a guard. P1's lead site fires only on a leading LONE never-given particle — a family piece with no base, exactly what condition (ii) rejects — so an exclusion was added to P1's redistribution, for tokens this site had already placed, and is now unreachable: 0 differences over 61,854 generated parses with it removed. It is deleted rather than kept defensively, and `tests/v2/pipeline/test_post_rules.py` pins the exclusivity with the name that needed it (`de la Vega de`). Recorded because the shape it prevented is real and would return if condition (ii) ever loosened.
REACH: 12 of 6600 parses move — the shipped 1100-name corpus × three `name_order` values × `middle_as_family` off and on — over four names (`Jong Anke de`, `Mesnil Garcia de`, `Berg Jan de Jr.`, `Mesnil Garcia Carlos de`), none under the default order. TWO OF THE FOUR ARE NAMES THIS PR'S OWN EXAMPLE LINES ADD, which is why the corpus is 1100 and not master's 1094: `corpus_rules.jsonl` regenerates from rules.md, so a rule that adds examples adds corpus names and then counts them. State the corpus with the count or the two drift apart — a first draft of this entry said "14 of 6564 ... 1094 corpus names" and by the time it shipped both numbers named a corpus the tree no longer had.
Recompute by parsing the four `tools/differential/corpus*.jsonl` files of THIS tree, deduped, under each of the six policies, and comparing the seven role fields against a checkout of the parent commit reading the SAME corpus files. The comparator is the pre-change code, never a construction of this rule's own. The count is a dated snapshot, measured 2026-08-30: nothing in the repository holds the parent tree.
THE DIFFERENTIAL GATE CANNOT SEE ANY OF IT, for a reason different from the render views' — `tools/differential/compare.py` parses every corpus name as `HumanName(name)` and sweeps no policy at all, so no non-default `name_order` behavior has ever been compared across versions. Gate output is byte-identical before and after at all three baselines apart from the corpus names the new example lines add, none of which diffs at any baseline. The issue's own line "Needs a differential pass: it changes parse output for shipped policies" is true about the change and wrong about the instrument.
The issue's headline number does not measure this shape. "464 inputs where the two orders differ in field content" is what permuting the given and middle positions DOES; measured on the pre-change tree over master's corpus it was 247 of 1094, and this change reduces it, so the number cannot be quoted in the present tense at all. The shape itself — a lone never-given particle standing alone in a non-family role beside a real family — was 5 rows over that corpus × 3 orders before the change, three of them the comma case `Jong, van der` that P6's words-to-spare guard declines on purpose.


### M2 — the maiden-marker rule

- 2026-07-03 (maiden-bucket design; #274 filed 2026-07-07, landed in the v2 core, PR #288) — the marker takes everything after it up to a trailing suffix, greedily: "née Jones Smith" is a two-word maiden name, matching how the marker is actually used in running text. The marker itself is dropped as structural, like a delimiter character.
Expand Down
73 changes: 66 additions & 7 deletions docs/design/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,15 @@ P1. Rationale: a never-given particle standing alone cannot be
declaring that order asserts that what follows the family is not
more surname. Opening the name under the default order, or
standing in the given position under any order, it takes the rest
of the name: nothing there marks where the surname ends. One name
of the name: nothing there marks where the surname ends. One
case of that second position is P6's rather than this rule's,
and only one: a particle ENDING the name under a declared
family-first order, where a family with a base and a name word
besides it both remain. P6 attaches it to that family and writes
it before the base. Everything else in the given position is
still this rule's fold, including the name that has nothing left
once the particle is taken — which is why "Mesnil de" reports
family "Mesnil de" here and not P6's "de Mesnil". One name
word means one UNIT — a particle chain (P2), a conjunction join
(P3) or a bound given-name pair (P5) is taken whole or not at
all. A title does not move the opening position (P4), but a
Expand Down Expand Up @@ -365,7 +373,9 @@ P6. Rationale: a particle ending the name has nothing to link
after a comma there is no signal that separates the two
readings. Dutch and Flemish names are listed exactly
this way ("Beethoven, Ludwig van"), the tussenvoegsel trailing
the given name but belonging to the surname.
the given name but belonging to the surname. The comma is not
the only thing that names the family: a declared family-first
order says the same, and the same listing is written both ways.
Where a family comma has already named the family, a particle
ending the name attaches to that family name and is written
before it — provided at least one given word remains, so that a
Expand All @@ -388,6 +398,42 @@ P6. Rationale: a particle ending the name has nothing to link
"Beethoven, Ludwig van" → family_base="Beethoven"
"Beethoven, Ludwig van" → family_particles="van"
"Nguyen, Van" → given="Van" · boundary
Without a comma, a declared family-first order has named the
family in the same way and the attachment fires there too. The
words-to-spare test is the same one, in the shape a positional
read needs it: a name word must remain ahead of the run BESIDES
the family it attaches to, so the one-given-word listing keeps
the reading it has either way — "Jong, de" and "Jong de" under a
family-first order both report family "Jong de", which is P1's
fold and not this rule. Two conditions are the comma's alone.
Only a NEVER-GIVEN particle attaches, an ambiguous one keeping
whatever reading its position gives it (P1). And the suffix
reading stands: a trailing abbreviation is a post-nominal until
a comma makes the tussenvoegsel commoner, so the run is found
among the pieces that hold a name, a trailing piece holding none
being walked past as it is there. The family the run attaches to
must have a base of its own — a word no particle vocabulary
claims (R2) — since a family that is all particles is not a
family written beside anything.
The pieces left over are laid out in the declared order for
their own number. At two of them that is family-then-given in
either family-first order, which is why the orders agree on the
shape this rule was filed for; at three they still differ, the
declared order being what decides given from middle.
"Jong Anke de" family-first → family="de Jong"
"Jong Anke de" family-first → given="Anke"
"Mesnil Garcia de" family-first → family="de Mesnil"
"Mesnil Garcia de" family-first → given="Garcia"
"Mesnil Garcia de" family-first-given-last → family="de Mesnil"
"Mesnil Garcia de" family-first-given-last → given="Garcia"
"Berg Jan de Jr." family-first → family="de Berg"
"Berg Jan de Jr." family-first → suffix="Jr."
"Mesnil Garcia Carlos de" family-first → given="Garcia"
"Mesnil Garcia Carlos de" family-first-given-last → given="Carlos"
"Berg Jan vd" family-first → suffix="vd"
"van der" family-first → family="van der" · boundary
"Mesnil de" family-first → family="Mesnil de" · boundary
"van Berg Jan de" family-first → given="Berg" · boundary
Accepted: an ambiguous particle attaches on the same terms as a
never-given one, so a Vietnamese name written in this listing
loses its given name — but only in the UNACCENTED
Expand Down Expand Up @@ -417,12 +463,22 @@ P6. Rationale: a particle ending the name has nothing to link
"Nguyen Thi Van" family-first-given-last → family="Nguyen"
"Nguyen Thi Van" family-first-given-last → given="Van"
"Nguyen Thi Van" family-first → middle="Van"
Accepted: without a family comma the name's written shape is not
settled — "Jong Anke de" may be a misformatted listing, and a
bare "Jong de" may be a given name beside a particle — so the
attachment is scoped to the comma form, and the comma-less
shapes keep their positional reading.
Accepted: under the DEFAULT order a comma-less name's written
shape is not settled — "Jong Anke de" may be a misformatted
listing, and a bare "Jong de" may be a given name beside a
particle — so nothing there names the family and the positional
reading stands. What the attachment needs is not the comma but
what the comma says; a declared family-first order says it, and
the default order says nothing.
"Jong Anke de" → family="de"
Accepted: the never-given restriction cuts across the Dutch
tussenvoegsel set, so the two formats agree for the never-given
`de` and not for the ambiguous `van`. The Accepted clause above
is what decides it: the comma-less family-first-given-last form
is the ONE format that reads the unaccented Vietnamese spelling
correctly, and an attachment taking ambiguous particles without
a comma would take that format too.
"Beethoven Ludwig van" family-first → middle="van"
Accepted: the precedence over S2 is stated for the shape, so it
sweeps in every word that is both particle and suffix
vocabulary — today vd, do and mc. Only vd's reading was
Expand All @@ -440,6 +496,9 @@ P6. Rationale: a particle ending the name has nothing to link
it as its pair first (P5), so the attachment never sees it —
unless the particle is of the unambiguous suffix vocabulary too
(vd, mc), which the join declines and the attachment then takes.
That last exception is the COMMA site's: without a comma the
suffix reading stands, so a family-first `Berg abdul vd` reports
suffix `vd` where `Berg, abdul vd` reports family `vd Berg`.
"Berg, abdul van" → given="abdul van"
"Berg, abdul vd" → family="vd Berg"
history: decisions.md#P6 · interacts: A1, C1, P1, S2, P5 · implemented: nameparser/_pipeline/_post_rules.py
Expand Down
Loading
Loading