Skip to content
Merged
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
19 changes: 19 additions & 0 deletions docs/design/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,25 @@ 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 #467 (the no-comma site, and the given position given back) — a declared family-first order names the family as a comma does, so P6's attachment fires without a comma too. It is keyed on TWO things, and review showed the second is not implied by the first: the run must stand in a MIDDLE — the one position that means nothing for a particle, middles being further given names — and it must END the name. Together they name the order without asking it: the default order ends with the FAMILY and FAMILY_FIRST_GIVEN_LAST with the GIVEN name, so only FAMILY_FIRST can put a name's last word in a middle. This DECIDES the parenthesis the 2026-08-16 Scope bullet left open ("arguably a missing comma under a declared family-first order") and narrows that bullet to the default order.
P1 LOSES ITS GIVEN-POSITION SITE in the same change, and that half is a correction rather than a narrowing. That slot holds what the caller DECLARED to be the given name; the never-given vocabulary supplies a reading where position leaves the question open and vetoes none that position has already given. `Ménil de` under either family-first order now reports given `de`.
THE PREMISE THIS REPLACES WAS FALSE, and it had been stated in three places (rules.md#P6's rationale, `_lexicon.py`, `config/particles.py`): that a never-given particle "cannot be a name at all". Measured, all 33 members report given == themselves when parsed bare. This is the SECOND correction of the same over-claim in this entry — the 2026-08-16 note above records dropping "no particle is a name by itself" for the identical reason. The conclusions drawn from it were mostly right; the reason was not, and it is load-bearing, because stating it as a property of the VOCABULARY invites the same inference for a particle ending the name, where there is nothing to join forward to and the writing is all there is.
NO VOCABULARY TEST AT THE NEW SITE, and that is the substance of the design. The comma path needs one because a comma cannot separate the Dutch reading from the Vietnamese; the declared order can, and does both at once — `Beethoven Ludwig van` under FAMILY_FIRST gives family 'van Beethoven' though `van` is ambiguous vocabulary, and `Nguyen Thi Van` under FAMILY_FIRST_GIVEN_LAST keeps given 'Van' though it is the same word in the same set. A never-given test here would have excluded 37 ambiguous particles — von, di, da, del, le, and `van` itself, the flagship word of the listing the rule is named for. An earlier attempt (#466) carried exactly that test and failed on the Dutch case because of it.
NO RE-LAYOUT EITHER, and this is a property of ONE order rather than a simplification: under FAMILY_FIRST the roles run family, given, middle, middle…, so dropping a trailing MIDDLE leaves every other piece's role untouched. It is false of FAMILY_FIRST_GIVEN_LAST (family, middle…, given) and of the default order, which is the second thing the ends-the-name test secures. #466 removed a piece and re-laid the leftover out, which lost a given name outright on `van Berg Jan de` and promoted a post-nominal into the given slot on `Berg Jan Jr. de`. Both defects are structurally unreachable here. The one guard that remains is the base: attaching to an all-particle family renders one particle in front of another, and R2 reads those words as ordinary name words, which no rule reorders.
THE INVARIANT THIS RESTS ON, and the reason to trust it over the argument it replaced: for a name of the form `Family Given [Particle]`, the family-comma writing under the default order and the comma-less writing under FAMILY_FIRST parse identically. Measured over ALL 70 particles × 3 families × 3 givens = 630 names: 0 of 630 agreed before this change, **603 of 630** after. The invariant draws the never-given/ambiguous line by itself instead of having it asserted, which is why no vocabulary test appears at the site.
The 27 failures are the three words that are BOTH particle and suffix vocabulary — `vd`, `mc`, `do` — and they are a real limit, not noise: P6's precedence over S2 is stated inside the comma paragraph and justified by the comma ("a trailing abbreviation after a family comma is the tussenvoegsel far more often"), so the comma form gives family 'vd Berg' where the comma-less family-first form gives suffix 'vd'. The rule now says so where it states the correspondence.
A FIRST DRAFT OF THIS ENTRY CLAIMED 216 of 216, and the number came from a script that sliced the particle vocabulary — `sorted(particles)[:14]` — which excluded exactly those three words. That is AGENTS.md's axis 2 ("your detector is a second, unreviewed implementation") in its purest form: the detector agreed with the design because it inherited the design's blind spot. Recompute over the WHOLE vocabulary, never a sample.
Scope the invariant to that SHAPE as well: 219 of the 304 comma-bearing corpus names parse differently with the comma simply removed, because a comma also delimits titles and suffixes. (An earlier draft said "181 of 247", which reproduces under no definition of "comma name" — the 304 matches the count the 2026-08-29 entry above already records.)
An asymmetry falls out of it rather than being carved: shape 2 places the given name BEFORE the middles, so it corresponds to FAMILY_FIRST only. There is no comma format that puts the given name last, so FAMILY_FIRST_GIVEN_LAST has no twin to inherit an equivalence from. That is why the trailing particle is one order's business (#469).
ONE EXAMPLE LINE WAS DELETED RATHER THAN UPDATED: `"Nguyen Thi Van" family-first → middle="Van"`. The name is written in FAMILY_FIRST_GIVEN_LAST; its FAMILY_FIRST reading is wrong by construction, so the line pinned a value nobody wants and nothing should depend on. Recorded here because the general question — whether a rules.md example should declare its format, and whether the doc test should refuse to run one outside it — is #470 and is NOT decided by this change.
REACH: 30 of 6606 parses move — this tree's 1101-name corpus × three `name_order` values × `middle_as_family` off and on — over twelve names, NONE under the default order. 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. Dated snapshot, measured 2026-08-30. Note the corpus grew from master's 1094 by this change's own example lines, which is why the count must name the tree it was taken on.
The differential gate cannot see any of it: `compare.py` parses every name as `HumanName(name)` and sweeps no policy, so no non-default `name_order` behavior has ever been compared across versions. Output is byte-identical at all three baselines apart from the added corpus names, none of which diffs.
WHAT REVIEW FOUND, because the corpus could not. Four agents on PR #472 found the site firing outside the order it was reasoned from, in two directions, and the differential corpus reported neither: 52 default-order names (a conjunction stops a particle's forward chain and leaves it standing in a middle — `Maria Luisa y de la Cruz` gave family 'de la Cruz' where master gives 'la Cruz') and 366 FAMILY_FIRST_GIVEN_LAST folds of a particle that does not end the name (`de Anke van y` gave family 'van de Anke' with the given name still behind it). Of 1099 corpus names only 6 hold a conjunction-then-particle adjacency and none in this shape, so the REACH recipe honestly reported 0 default-order movers throughout. The count was right and the population was blind; "none under the default order" was a property of the corpus, not of the code. Both are now `· boundary` examples in rules.md so the doc runner holds them.
The repair is one clause — the run must END the name — and it subsumes the order test a first repair added: measured over 542,592 generated parses, an explicit `state.order[0] is Role.FAMILY` never decides anything the ends-the-name test has not already decided, so it is not in the code. What IS kept is `state.order is not None`, which is what keeps this site off the family-comma path (0 co-firings with P6's comma site over ~892,000 parses).
A stale index list was repaired with it: O1 and O2 retag between roles without recomputing, and this site was the first consumer of `families` after them (33,840 stale observations instrumented). No shipped input reaches a wrong answer through it — no `_TURKIC`/`_EAST_SLAVIC`-matching word is particle vocabulary — so the fix is unwitnessed by any test and is recorded here instead.
Open: [#471](https://github.com/derek73/python-nameparser/issues/471) is the leading fold's REACH under the default order (`de Mesnil Juan` reports the whole string as the family). Split out of this change deliberately: it is a 1.4.0 parity break on the order every existing caller is on — prototyped, 5 unexplained diffs at that baseline and a failing v1 parity test — where nothing under a family-first order has a v1 answer at all.


### 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
Loading