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
47 changes: 34 additions & 13 deletions docs/customize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -288,15 +288,27 @@ listed below.
* - ``maiden_delimiters``
- ``frozenset[tuple[str, str]]``
- Routes content enclosed by these delimiter pairs to ``maiden``
instead, and drops them from the effective nickname set. A
marker word opening the enclosed content is dropped from the
value, so ``"Jane Smith (née Jones)"`` gives maiden ``Jones``
— but only where that content holds more than one *token*,
since a lone ``"(Nee)"`` is a maiden name rather than a
marker. Tokens, not words: a marker written against the name
it marks is one token with them, so ``"山田花子(旧姓佐藤)"``
keeps its ``旧姓``. Defaults to empty — see the routing
example below.
instead, and drops them from the effective nickname set. Set
this for a clause that says nothing about itself, which is two
kinds of clause and not one: content with no marker word in it
(``"Cherice J. (Johnson) Williams"``, the parenthesized birth
surname written bare) AND a lone marker word
(``"Jane Smith (Nee)"``, which reads nickname ``Nee`` by default
and maiden ``Nee`` only with the pair listed here). What needs
no configuration since 2.2 is a clause that opens with a marker
word AND has a word after it: ``"Jane Smith (née Jones)"`` reads
maiden ``Jones`` whatever pair encloses it, unless the content is
suffix-shaped, which is taken ahead of both: the brackets are
dropped and the content parses as if written bare, so
``"Jane Smith (née Jr.)"`` gives family ``née``, suffix ``Jr.``
rather than a suffix of the whole clause. A marker word opening
the enclosed content is dropped from the value either way, but
only where that content holds more than one *token* — the same
reason a lone ``"(Nee)"`` listed here keeps ``Nee`` as the
maiden value rather than reading it as a marker. Tokens, not words: a marker written
against the name it marks is one token with them, so
``"山田花子(旧姓佐藤)"`` keeps its ``旧姓``. Defaults to empty —
see the routing example below.
* - ``extra_suffix_delimiters``
- ``frozenset[str]``
- Adds separators that split suffix groups, e.g. ``" - "`` for
Expand Down Expand Up @@ -496,10 +508,19 @@ off.
Nicknames, maiden names, and brackets
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

A delimiter pair routes to exactly one field, and ``maiden_delimiters``
states the more specific intent — so listing a pair there drops it from
the effective ``nickname_delimiters`` set automatically, and the
one-liner is the whole recipe:
A delimiter pair carries no meaning of its own, so what a clause reads
as is settled in steps. Suffix-shaped content is taken first: the
brackets are dropped and what was inside parses as if it had been
written bare, which is not the same as the clause becoming the suffix
(``"Jane Smith (née Jr.)"`` gives family ``née``, suffix ``Jr.``).
Then the content is asked whether it announces itself: a
clause opening with a recognized maiden marker and carrying a word
after it is a maiden name whatever encloses it, and needs nothing
configured. Only for what is left — markerless content, and a lone
marker word — does the PAIR decide, and that is what this knob is for.
Listing a pair here drops it from the effective
``nickname_delimiters`` set automatically, and the one-liner is the
whole recipe:

.. doctest::

Expand Down
17 changes: 15 additions & 2 deletions docs/design/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,8 @@ Excluded (MAIDEN_MARKERS, per nameparser/config/maiden_markers.py):
#274 flagging "is nee safe as a default (it's also a rare
surname)" as open; the question resolved silently with the shipped set. Recorded here because the included risky member deserves its analysis as much as the excluded ones; M1's (Nee) boundary covers only the enclosure path, not this marker path.
- "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.
- 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.
- 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, on its own merits rather than following the feminine form in.
- 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.


### C1 — the suffix-comma decision
Expand Down Expand Up @@ -491,7 +492,19 @@ Declined:

- 2026-08-05 #329/#335 — marker auto-detection inside a nickname-delimited clause was deferred to #335 on a corpus measurement: 山田 花子(旧姓 佐藤) is in the CJK differential corpus so the #329 change was gate-visible, while "Jane Smith (née Jones)" is in no corpus — shipping auto-detection in 2.1 would have let a real Latin-affecting change ride under a "0 Latin-only" gate report.

Open: [#335](https://github.com/derek73/python-nameparser/issues/335) should a marker inside a NICKNAME-delimited clause flip it to maiden without configuration.
- 2026-08-26 #335 — correction to the 2026-08-05 entry above, which reasoned from "the #329 change was gate-visible". It was not. Under the default policy the corpora run, no name reached the drop pass at all, so 山田 花子(旧姓 佐藤) moving between 2.0.0 and 2.1.0 was the East Asian order flip and not #329. Nor is #329 gate-visible now that M3 routes marker-led clauses to maiden without configuration: a ledger rule narrows by which FIELDS move and never by what they hold, so reverting the drop leaves the same six names moving the same fields and all three gates green (measured by mutation 2026-08-26). The {nickname, maiden} pair is the 2.1.0 ledger's; the other two baselines classify the CJK name differently, which is why those ledgers are split and must stay so. The deferral #329 argued for was still the right call — it was about a Latin-affecting change riding under a gate report, and that half held — but the premise is corrected here rather than left to be re-used. tools/differential/README.md carries the same correction beside the table it belongs to.
- 2026-08-26 #335 (M3) — the opt-in this section opens with narrowed, and the boundary is not where the 2026-07-03 entry drew it. A clause whose content announces itself no longer needs its pair declared, so what configuring maiden_delimiters buys is now markerless content and one-word clauses. See #M3 for the whole reasoning; M1 still governs every clause whose pair is configured, and M3 defers to it for the marker drop itself.

### M3 — the marker-led clause

- 2026-08-26 #335 (M3, PR on fix/335-maiden-marked-clause) — a bracketed clause whose content opens with a recognized marker word and carries a word after it reads as the maiden name, whichever bucket the enclosing pair sits in. The parse already had the information: "Jane Smith née Jones" gave maiden "Jones" while "Jane Smith (née Jones)" gave nickname "née Jones", because ( ) is a nickname pair by default and nothing looked inside it. The clause says "maiden" out loud, so the caller should not have to say it in Policy.
- 2026-08-26 #335 — the MECHANISM, and the issue proposed the other one. Masking the delimiters and letting M2's bare-marker rule consume the content would avoid a second implementation, and it was rejected on measurement: the two readings agree on a trailing clause and diverge on an interior one. "Jane (née Jones) Smith" reads given "Jane", family "Smith", maiden "Jones" under extraction; masking turns it into the bare "Jane née Jones Smith", which reads maiden "Jones Smith" and family "" (measured 2026-08-26), because M2's take runs to the end of the name. The closing delimiter is a boundary the writer typed and masking throws it away. Extraction was also the SMALLER change — extract already emits Role.MAIDEN regions and group already drops the marker inside one (#329), so this is a role swap at the existing emit site plus one predicate, in the branch S1 has already declined. That placement is what keeps S1 first, but be exact about the mechanism, because the obvious mutation does not test it: hoisting the swap above the _suffix_shaped call changes nothing at all (measured 2026-08-26), since the S1 branch never reads `role` -- it masks the two delimiter spans and hands the content to the token stream, and the role it would have carried is never consulted. What inverts the precedence is giving M3 the clause first, an early `extracted.append((Role.MAIDEN, inner))` ahead of the S1 test: that reads "Jane Smith (née Jr.)" as family "Smith", maiden "Jr." instead of family "née", suffix "Jr.", and M3's own example line for it fails. So the precedence is pinned, and by that example rather than by the branch shape.
- 2026-08-26 #335 — the condition is a word AFTER the marker, not the marker alone. The reason is M1's: a one-word clause keeps its word because Nee is an attested surname (Irish Ní/Nee, and a Chinese romanization), and a marker with nothing after it marks nothing. Be precise about what pins it, because the obvious answer is wrong: M1's own boundary line carries the maiden-parens annotation, so it asserts the CONFIGURED reading and passes either way — built and measured 2026-08-26, a len(words) > 0 predicate still gives it maiden "Nee". What the loosened predicate actually breaks, measured by mutation 2026-08-26, is M3's own boundary example ("Jane Smith (née)" → nickname "née") and tests/v2/cases.py::maiden_marked_clause_one_word_stays_a_nickname in both runners — three pytest failures — AND the 2.1.0 differential gate, which goes red with three unexplained names: "Jane Smith (Nee)", "Jane Smith (Nee) (Jones)" and "Jane Smith (née)". The gate is the stronger signal and the one to look at first. The middle name is why: loosened, it reads maiden "Nee" and nickname "Jones", splitting one bracket pair into the maiden field and the next into the nickname field, which is the direct contradiction of M1's "two enclosures read as one maiden name". Anyone re-litigating the second-word test should check those, not M1's line. So the real division of labour is NOT markerless versus marker-led, which is how it was first written into rules.md and had to be corrected at review: it is one word versus a marker plus a word. "Jane Smith (Nee)" is marker-led and still reads nickname "Nee" by default, maiden "Nee" only with the pair configured (both measured 2026-08-26).
- 2026-08-26 #335 — accepted consequence: the word taken after the marker is not tested for being a name word, so M3 does not stop at a suffix word where M2's bare take does. "Jane Smith (née V)" reads maiden "V" while the bare "Jane Smith née V" reads suffix "V" and family "née" (both measured). The reason is structural rather than an oversight: M3 hands the WHOLE clause to Role.MAIDEN and the bracket is its right boundary, so the trailing-suffix rule never sees the word as trailing the name at all. Pinned as M3's Accepted line in rules.md, which puts "Jane Smith (née V)" in the differential corpus and classified in all three ledgers.
- 2026-08-26 #335 — rules.md#M3 lists implemented: nameparser/_pipeline/_extract.py alone, and that is deliberate rather than an omission. M3 decides a REGION'S ROLE; the marker drop it describes ("the marker itself dropped, as M1 drops it") is M1's mechanism in _group.py, which cites M1 and is governed by it. Adding _group.py to M3's list would mean inventing an M3 citation there for behavior M1 already owns — and test_doc_citations requires implemented: to equal the set of modules that cite the rule, so the two would have to move together. Do not re-open this.
- 2026-08-26 #335 — the durable finding, and the one worth reading before touching either side: _maiden_marked asks a WHITESPACE-SPLIT, _normalize'd question of the clause's first word, while tokenize and classify ask a token-level one. _normalize strips a trailing period but not a comma, so 'née,' is not a marker to M3 while the tokenizer splits the comma off and still tags the token vocab:maiden-marker. That divergence is the only thing keeping the "role is not Role.MAIDEN" branch of _group.group's clause-scoped drop pass reachable at all: without it, a marker-led NICKNAME clause could no longer exist, since M3 would have converted every one to MAIDEN before group ran. Anyone "unifying" the two tests would orphan that filter silently. The pin is tests/v2/cases.py::marker_glued_to_punctuation_keeps_the_clause_a_nickname, and it needs BOTH of its clauses — the drop pass is gated on the name holding a maiden region at all, so a marker-glued nickname clause standing alone leaves the branch unexercised (measured by mutation, 2026-08-26). The row that used to pin the filter, maiden_marker_delimited_beside_a_nickname_clause, lost the job to M3: both its clauses are marker-led, so both are maiden now and there is no contrast left in it.
- 2026-08-26 #335 — M3 makes the marker vocabulary act in a THIRD position. M2's claim acts on the word after a name word; M1's on the content of a configured pair; M3's now on the first word of any bracketed clause, under the default policy. #vocabulary-collisions C-i is keyed on the position a claim acts on, so extending where a rule acts re-asks C-i of the whole set it reads, and nothing in the process prompts that — the rule was designed, reviewed and approved without the set being re-examined. Review afterwards found one entry that fails C-i, the Czech abbreviation roz, and its disposition is this file's Excluded (MAIDEN_MARKERS) block, which is keyed to the vocabulary set rather than to any rule: it was removed. Two things worth separating, since the finding surfaced during this work and is easy to file under it — the defect was M2's and predates #335 (bare "Rosalind Roz Smith" lost its family name on master), and M3's own boundary already excluded the common one-word "(Roz)" spelling. What belongs to M3 is only the widening, and the lesson is the general one: re-ask C-i of a vocabulary set whenever a rule extends where its claim acts.
- 2026-08-26 #335 — what configuring maiden_delimiters still buys, since M3 removes the commonest reason to reach for it: markerless clauses, and one-word clauses. "Cherice J. (Johnson) Williams" is a real US convention and a corpus name, and nothing in that clause says maiden, so it stays a nickname by default — only a caller who knows their data can say otherwise. The knob is a declaration about the DATA; M3 is a reading of what a clause declares about itself.

### O1 — East Slavic rotation

Expand Down
Loading