From 78458202470bb544119321f50ccc67532dd983b1 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Mon, 31 Aug 2026 15:20:13 -0700 Subject: [PATCH 01/10] docs(release_log): cut the 2.2.0 entry to what a caller needs to decide The 2.2.0 section had grown to 12,442 words against 2,222 for 2.1.0 and 2,931 for 2.0.0 -- 5.6x the previous minor across fewer bullets, so the bloat was entirely per-bullet (150-771 words each, against 50-150 in 2.1.0). Bullets had become mini design documents: blast radius by baseline, per-field redistribution, gate-blindness disclaimers, recompute recipes, and review history. All of that already lives in docs/design/decisions.md, in more detail -- R3 carries #408's parity counts, recompute recipe and the gate-blindness note; parse-cost carries the +67 calls and the per-PR attribution; P2 carries #367's grouping argument. So this is deletion plus a pointer, not a migration. Pointers name the entry ("the R3 entry of docs/design/decisions.md"), never a line number, following the four bullets that already did. What each bullet keeps: how a name parsed before, what it does now, the limits a caller would trip over, the 1.4.0 direction, and the issue -- which carries the full measurement. 12,442 -> 4,927 words. Also replaces the roz restoration recipe's dataclasses.replace spelling with Lexicon.default().add(maiden_markers={"roz"}), the idiom customize.rst documents; verified to restore the reading exactly. Co-Authored-By: Claude Opus 5 --- docs/release_log.rst | 115 +++++++++++++++++++++---------------------- 1 file changed, 55 insertions(+), 60 deletions(-) diff --git a/docs/release_log.rst b/docs/release_log.rst index 6b427352..69618aa6 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -4,99 +4,94 @@ Release Log nameparser 2.2 finishes the 2.0 rename at the layer it never reached. The word lists in ``nameparser.config`` were still named - for v1's fields — prefixes, first names — while the - ``Lexicon`` they feed has spoken of particles and given names since - 2.0. They now agree. The lists are also frozen, which retires - editing one in place as a way to change a default and replaces it - with configuring a ``Lexicon`` or a private ``Constants``. - - Nothing moved between vocabularies. The rename itself changes no - parse at all; the parsing changes below are separate fixes, and - most of them reach the default name order -- each bullet names - the shapes and the corpus names it moves, and the gate output is - the source of record. The unusual part is the family-first - orders, which change too: seven names from the family-first fix, - twelve more from the trailing-particle change below (#467), which - is family-first only and moves nothing under the default order, - plus the names #367's title fix moves, which it moves in every - order. The other title fix below, #410, is default-order only -- - under either family-first order the word behind the title is - already the family, so the rule is a no-op there and moves - nothing (measured over all four corpora). The corpora have roughly - doubled across this cycle (#414 added the rules doc's examples, - #413 taught the issue harvester to read backticks), so the counts - in the bullets below are the classified summary at the time each - was written; the gate output is the source of record. - What breaks is code that *writes* to a default word list, and code + for v1's fields — prefixes, first names — while the ``Lexicon`` + they feed has spoken of particles and given names since 2.0. They + now agree, and the lists are frozen, which retires editing one in + place as a way to change a default. + + Nothing moved between vocabularies, and the rename itself changes + no parse. The parsing changes below are separate fixes. Most reach + the default name order; a bullet says so where its change is + family-first only or default-order only. Each names the shapes it + moves, the issue it closes carries the full measurement, and the + differential gate's output is the source of record for how many + names move. + + What breaks is code that *writes* to a default word list. Code that imports one by its 1.x name has until 3.0. **Breaking Changes** - - Add ``docs/design/`` contributor documentation: ``rules.md`` (the parser's normative rules with executable examples), ``decisions.md`` (the decision record), and ``mechanisms.md`` (the solution-pattern catalog), enforced by new tests that execute every documented example and verify every code citation; committed docstrings no longer reference gitignored planning documents - - Change every vocabulary set in ``nameparser.config`` to a ``frozenset``: ``TITLES``, ``GIVEN_NAME_TITLES``, ``SUFFIX_WORDS``, ``SUFFIX_ACRONYMS``, ``SUFFIX_ACRONYMS_AMBIGUOUS``, ``GLUED_HONORIFICS``, ``PARTICLES``, ``NON_GIVEN_NAME_PARTICLES``, ``BOUND_GIVEN_NAMES``, ``CONJUNCTIONS`` and ``MAIDEN_MARKERS`` (``KOREAN_SURNAMES`` already was one). Editing one in place -- ``TITLES.add("dean")``, the old way of changing a global default -- now raises ``AttributeError: 'frozenset' object has no attribute 'add'`` at the line that writes it. It was never a reliable way to change a default: whether an edit reached a given parse depended on which config objects had already been built, so one program could hold two disagreeing defaults with nothing to say so. To change the defaults for ``HumanName``, build a private ``Constants`` and pass it (``c = Constants(); c.titles.add("dean"); HumanName(name, constants=c)``); mutating the shared ``CONSTANTS`` still works, but warns and goes away in 3.0. For the 2.0 API, build a lexicon and pass it to a parser (``Parser(lexicon=Lexicon.default().add(titles={"dean"}))``). Neither is affected by this change. ``CAPITALIZATION_EXCEPTIONS`` is a mapping, not a set, and is unchanged. See :doc:`migrate` and :doc:`customize` (#293) + - Add ``docs/design/`` contributor documentation: ``rules.md`` (the parser's normative rules, with executable examples), ``decisions.md`` (the decision record) and ``mechanisms.md`` (the solution-pattern catalog). New tests execute every documented example and verify every code citation + - Change every vocabulary set in ``nameparser.config`` to a ``frozenset``. Editing one in place -- ``TITLES.add("dean")``, the old way of changing a global default -- now raises ``AttributeError`` at the line that writes it. It was never a reliable way to change a default: whether an edit reached a given parse depended on which config objects had already been built, so one program could hold two disagreeing defaults with nothing to say so. To change the defaults for ``HumanName``, build a private ``Constants`` and pass it (``c = Constants(); c.titles.add("dean"); HumanName(name, constants=c)``); for the 2.0 API, build a lexicon (``Parser(lexicon=Lexicon.default().add(titles={"dean"}))``). Mutating the shared ``CONSTANTS`` still works, but warns and goes away in 3.0. ``CAPITALIZATION_EXCEPTIONS`` is a mapping, not a set, and is unchanged. See :doc:`migrate` and :doc:`customize` (#293) **Behavior Changes** - - Fix a title changing how the name behind it is read. A leading particle deliberately does not chain onto the words after it -- that is what makes ``"Van Johnson"`` read as given ``Van``, family ``Johnson`` rather than as one surname -- but the exception was keyed on the first *piece of the input*, so a title displaced the particle out of that position and the chain fired. Identical name text parsed two ways depending on whether a title preceded it: ``"Dr. Van Johnson"`` gave family ``Van Johnson`` with no given name, and ``"Sir Van Johnson"`` gave given ``Van Johnson`` with no *family* name at all, a given-name title then handing the whole chain to ``given``. A title is not part of the name, so it can no longer decide whether the name begins with a particle: the exception is keyed on the first piece of the **name**, and a titled name now reads exactly as the same text reads untitled. What moves is every titled name whose first name-piece is a particle. Every reading in this bullet is the default ``name_order``, which is where the fields differ by order: ``"Dr. Van Johnson"`` and ``"Mr. Van Nguyen"`` go to given ``Van`` plus family ``Johnson``/``Nguyen``, ``"Dr. Van Johnson Smith"`` to given ``Van``, middle ``Johnson``, family ``Smith``, and ``"Sir de Mesnil"`` to family ``de Mesnil``. The same grouping change reaches ``Policy(name_order=FAMILY_FIRST)`` and ``Policy(name_order=FAMILY_FIRST_GIVEN_LAST)``, where the leading particle takes the family rather than the given: ``"Dr. Van Johnson"`` reads family ``Van``, given ``Johnson`` under both. Three fields carry nearly all of the redistribution -- ``given``, ``middle`` and ``family`` -- but two more move on names inside this class, and only ``suffix`` and ``nickname`` move on none of them. ``title`` GROWS where the word that stops the transparency scan is one the chain used to swallow: ``"Dr. St John Smith"`` was title ``Dr.``, family ``St John Smith`` and is now title ``Dr. St``, given ``John``, family ``Smith``, ``st`` being in both vocabularies and so staying a title piece instead of being chained onto the name. ``maiden`` appears where un-chaining lets a marker standing behind the particle be seen at all: ``"Mr. Van Johnson nee Brown"`` was family ``Van Johnson nee Brown`` with no maiden name and is now given ``Van``, family ``Johnson``, maiden ``Brown``. Untitled names are untouched, and so is every name with no particle behind its title: ``"Mr. Smith"``, ``"Mr. John Smith"``, ``"Sir Ian McKellen"`` and ``"King Henry"`` are byte-identical. So is a name whose leading word is *both* a title and a particle -- ``st``, ``do`` and ``freiherr`` are each in both vocabularies, so ``"St John Smith"``, ``"Do John Smith"`` and ``"Freiherr von Richthofen"`` keep their readings: a word that could be the name's own first piece stops the transparency scan rather than being stepped over. The fork is still reported, and still as ``PARTICLE_OR_GIVEN``; it now comes from the same place the untitled ``"Van Johnson"`` reports it, so the ``detail`` text changes from "was chained onto the following name piece" to "leading 'Van' may be a family-name particle; read as a given name". This fixes a defect v1 shipped as a known-failing test for the life of the 1.x line, with the note "Not sure how to fix this without breaking Mr and Mrs"; it does not break Mr and Mrs, which have no particle to displace. One of the 751 differential corpus names moves, at the 1.4.0, 2.0.0 and 2.1.0 baselines alike -- and it is the same one name under each family-first order too, ``"Mr. Van Nguyen"``. One case regressed along the way -- ``"Sheik Abu Bakar"`` was given ``Abu Bakar`` and became given ``Abu``, family ``Bakar`` -- because it had read correctly only as a side effect of the bug: ``abu`` happens to be a particle as well as a bound given name, and ``"Sheik abdul salam"`` shows that being a bound given name alone does not chain. The #369 fix below restores it by the rule that should have read it all along (closes #367) + - Fix a title changing how the name behind it is read. ``"Dr. Van Johnson"`` gave family ``Van Johnson`` with no given name, and ``"Sir Van Johnson"`` gave given ``Van Johnson`` with no family at all; both now read given ``Van``, family ``Johnson`` -- the reading the untitled ``"Van Johnson"`` has always had. A leading word that is *both* a title and a particle is unchanged: ``"St John Smith"``, ``"Do John Smith"`` and ``"Freiherr von Richthofen"`` keep their readings. This fixes a defect v1 shipped as a known-failing test for the life of the 1.x line. See the ``P2`` entry of ``docs/design/decisions.md`` (closes #367) - - Fix a given-name title keeping a bound given name from joining the word after it, so ``"Sheik abdul salam"`` read given ``abdul``, family ``salam``. A bound given-name word such as ``abdul`` joins the next word into one given name only when a name word is left to spare for the family, and a title is not a name word, so behind ``Sheik`` two words did not join. But ``Sheik`` is a *given-name* title -- the same vocabulary that keeps ``"Sir John"`` a given name with no family -- and that membership asserts that a given name follows, so behind it there is no family to spare: ``"Sheik abdul salam"`` now reads given ``abdul salam`` with an empty family, as ``"Sir John"`` does, and ``"الشيخ عبد الله"`` reads given ``عبد الله``. A title that addresses by family keeps the reserve -- ``"Dr. abdul salam"`` is unchanged -- and the title run is matched as one key, as the ``"Sir John"`` rule matches it. This also restores ``"Sheik Abu Bakar"`` to given ``Abu Bakar``, the reading the fix above had regressed, and by the bound given-name join rather than the particle chain; with the join taking the word, the ``PARTICLE_OR_GIVEN`` ambiguity that name reported through 2.1 is no longer reported. Two names of the differential corpora move at every baseline, ``"Sheik abdul salam"`` and its ``"Sheik abdul salam Jr"`` spelling from the rules examples; ``"Sheik Abu Bakar"`` is byte-identical to 1.4.0 again and differs from 2.0.0 and 2.1.0 in its ambiguity report alone. Under ``Policy(name_order=FAMILY_FIRST)`` and ``FAMILY_FIRST_GIVEN_LAST`` the join is the same and the pair lands in the family instead -- ``"Sheik abdul salam"`` reads family ``abdul salam`` with no given name, as ``"Sir John"`` reads family ``John`` there -- so the same two names move under each family-first order too (closes #369) + - Fix a given-name title keeping a bound given name from joining the word after it. ``"Sheik abdul salam"`` read given ``abdul``, family ``salam``, and now reads given ``abdul salam`` with an empty family, as ``"Sir John"`` does; ``"الشيخ عبد الله"`` reads given ``عبد الله``. A title that addresses by family is unchanged (``"Dr. abdul salam"``). This also restores ``"Sheik Abu Bakar"`` to given ``Abu Bakar``, which the fix above had regressed, and drops the ``PARTICLE_OR_GIVEN`` ambiguity that name reported through 2.1 (closes #369) - - Fix a bound given name swallowing the family name before a single-letter generational suffix: ``"abdul Smith V"`` read given ``abdul Smith`` with no family, where ``"abdul Smith II"`` and ``"abdul Smith Jr"`` read correctly. The join reserves a name word for the family, and a bare ``V`` is both suffix vocabulary and initial-shaped, so the reserve counted it as a name word while the suffix rule then read it as the suffix it is -- the family the reserve spared was never there. The reserve now asks the question the suffix rule answers, and the two stages share the one predicate for it: ``"abdul Smith V"`` reads given ``abdul``, family ``Smith``, suffix ``V``, and so do ``I`` and ``X``, for every bound given-name word; ``"Sir abdul V"`` reads suffix ``V`` too, and a suffix word before the numeral does not hide it (``"abdul Smith Jr V"`` reads family ``Smith``). Shipped since 1.x: 1.4.0 read first ``abdul Smith``, last ``V``. No name of the differential corpora has the shape; the rules examples added with this fix carry it, and move against every baseline (closes #401) + - Fix a bound given name swallowing the family name before a single-letter generational suffix. ``"abdul Smith V"`` read given ``abdul Smith`` with no family, where ``"abdul Smith II"`` and ``"abdul Smith Jr"`` read correctly; it now reads given ``abdul``, family ``Smith``, suffix ``V``, and so do ``I`` and ``X``, for every bound given-name word. A suffix word before the numeral no longer hides it (``"abdul Smith Jr V"`` reads family ``Smith``). Shipped since 1.x: 1.4.0 read first ``abdul Smith``, last ``V`` (closes #401) - - Fix a bound given name joining a suffix as "the word after it": ``"abdul Jr Smith Berg"`` read given ``abdul Jr``, and where the suffix was the split credential the bound word joined *into* it -- ``"abdul Ph. D. Smith Berg"`` read suffix ``abdul Ph. D.``, a 2.0 regression -- 1.4.0 extracted the credential before parsing and read suffix ``Ph. D.`` with given ``abdul Smith``. The join now declines a suffix piece the way it already declines a maiden marker: ``"abdul Jr Smith Berg"`` reads given ``abdul``, middle ``Jr Smith`` (where ``"John Jr Smith Berg"`` puts it), ``"abdul Ph. D. Smith Berg"`` reads given ``abdul``, middle ``Smith``, suffix ``Ph. D.`` -- the suffix field restored, the given deliberately not, since the join takes the word after it or nothing rather than reaching past a credential -- and ``"Berg, abdul Jr Smith"`` reads suffix ``Jr``. Both shapes move against every baseline once the rules examples carry them; no name of the differential corpora had either (closes #421) + - Fix a bound given name joining a suffix as "the word after it". ``"abdul Jr Smith Berg"`` read given ``abdul Jr`` and now reads given ``abdul``, middle ``Jr Smith``, where ``"John Jr Smith Berg"`` puts it. Where the suffix was a split credential the bound word joined *into* it -- ``"abdul Ph. D. Smith Berg"`` read suffix ``abdul Ph. D.``, a 2.0 regression -- and now reads given ``abdul``, middle ``Smith``, suffix ``Ph. D.`` (closes #421) - - Fix a bound given name joining past a credential that the suffix rule then takes, leaving no family: ``"abdul Smith Jr Ma"`` read given ``abdul Smith`` with no family, where ``"John Smith Jr Ma"`` reads family ``Smith`` with suffix ``Jr, Ma``. The join's reserve had re-derived the suffix rule's trailing walk by hand and left out one of its two forks -- a bare acronym such as ``Ma`` is a credential only with words to spare -- so it counted the acronym as a name word, joined, and the suffix rule then peeled it. The two stages now share one walk: the reserve tries the join, runs the suffix rule's own peel over the pieces as the join would leave them, and joins only when two name words became one and nothing else changed. ``"abdul Smith Jr Ma"`` reads family ``Smith``, suffix ``Jr, Ma``; ``"abdul Smith Ma"`` reads family ``Smith``, suffix ``Ma`` rather than joining the pair and keeping the credential as the family -- both as 1.4.0 read them and as ``John`` reads in their place; ``"abdul Smith Berg Ma"`` keeps its join, and a title word standing after the bound word joins again as it did in 1.4.0 (``"Berg, abdul Sir"`` reads given ``abdul Sir``) without the pair ever being read as a title (``"abdul Sheikh and Ahmad Bakar"`` reads given ``abdul Sheikh and Ahmad``). No name of the differential corpora has the shapes; the ``Ma`` rules examples carry the fix, byte-identical to 1.4.0 and moving against 2.0.0 and 2.1.0, and the title-word shapes are pinned in tests (closes #425) + - Fix a bound given name joining past a credential that the suffix rule then takes, leaving no family. ``"abdul Smith Jr Ma"`` read given ``abdul Smith`` with no family and now reads family ``Smith``, suffix ``Jr, Ma``, as ``"John Smith Jr Ma"`` does; ``"abdul Smith Ma"`` reads family ``Smith``, suffix ``Ma``. Both as 1.4.0 read them. ``"abdul Smith Berg Ma"`` keeps its join, and ``"Berg, abdul Sir"`` still reads given ``abdul Sir`` (closes #425) - - Remove the Czech/Slovak abbreviation ``roz.`` from the default maiden markers, where it had been eating English given names. Marker matching is whole-token, case-folded and period-insensitive, so ``Roz`` -- the ordinary diminutive of Rosalind -- was the same string as the marker, and a maiden marker takes every word after it: ``"Rosalind Roz Smith"`` read maiden ``Smith`` with **no family name at all**, and ``"Rosalind Roz Jones Smith"`` read maiden ``Jones Smith``. Both now read the way 1.4.0 read them, given ``Rosalind`` with ``Roz`` a middle name and ``Smith`` the family. The defect is as old as the marker vocabulary and belongs to the bare marker rule, not to the bracketed-clause change in the bullet below; it was found while reviewing that change, which would have widened it -- with ``roz`` still shipped, ``"Jane Smith (Roz Jones)"`` reads maiden ``Jones`` where 2.1.0 read nickname ``Roz Jones``. What stops working is the abbreviation itself, for Czech and Slovak callers: ``"Anna Nováková roz. Svobodová"`` now reads middle ``Nováková roz.``, family ``Svobodová`` rather than family ``Nováková``, maiden ``Svobodová`` -- which is also how 1.4.0 read it, so this is a return to the v1 reading rather than a new one. The full participle is untouched and needs no configuration: ``"Anna Nováková rozená Svobodová"`` still reads family ``Nováková``, maiden ``Svobodová``, and it is only the abbreviation that was ambiguous. A caller who wants the abbreviation back adds it to their own lexicon -- ``Parser(lexicon=dataclasses.replace(lex, maiden_markers=lex.maiden_markers | {"roz"}))`` -- which restores the old reading exactly. This removal takes the default set to 16 entries. No differential corpus name contains the string in any case or position, so nothing moves against any baseline (closes the collision found in #335's review) + - Remove the Czech/Slovak abbreviation ``roz.`` from the default maiden markers, where it had been eating English given names. Marker matching is case-folded and period-insensitive, so ``Roz`` -- the diminutive of Rosalind -- was the same string as the marker, and a marker takes every word after it: ``"Rosalind Roz Smith"`` read maiden ``Smith`` with **no family name at all**. It and ``"Rosalind Roz Jones Smith"`` now read as 1.4.0 read them. What stops working is the abbreviation itself, for Czech and Slovak callers: ``"Anna Nováková roz. Svobodová"`` reads middle ``Nováková roz.``, family ``Svobodová``, which is also 1.4.0's reading. The full participle is untouched -- ``"Anna Nováková rozená Svobodová"`` still reads maiden ``Svobodová`` -- and a caller who wants the abbreviation back adds it to their own lexicon: ``Parser(lexicon=Lexicon.default().add(maiden_markers={"roz"}))`` (found in #335's review) - - Add the Polish maiden marker ``z domu`` to the default vocabulary, and let a ``maiden_markers`` entry be more than one word. ``"Maria Kowalska z domu Nowak"`` now reads family ``Kowalska``, maiden ``Nowak``, where every earlier version read the marker as part of the name (1.4.0: middle ``Kowalska z domu``, family ``Nowak``). The bracketed spelling moves with it -- ``"Maria Kowalska (z domu Nowak)"`` read nickname ``z domu Nowak`` and now reads family ``Kowalska``, maiden ``Nowak`` -- while a clause holding nothing but the marker stays a nickname, as a lone ``(née)`` does: ``"Maria Kowalska (z domu)"`` keeps nickname ``z domu``. **This changes what a caller who worked around the old limit should have configured.** Through 2.1 a multi-word entry in any vocabulary field could never match, and the ``UserWarning`` it raised said to split it into separate entries. For maiden markers that advice was wrong, not merely unhelpful: with ``z`` and ``domu`` stored as two entries, ``"Maria Kowalska z domu Nowak"`` reads maiden ``domu Nowak`` -- the marker's own second word inside the value -- and ``"Anna z Nowak"`` reads maiden ``Nowak`` and loses its surname, because a bare preposition is then a marker and a marker takes every word after it -- family ``Anna`` today, the given name standing in for the surname the clause ate, and no family at all before #445 below. If you added ``z`` and ``domu`` separately, remove both: the shipped phrase does the job, and dropping the two word entries is what restores ``"Anna z Nowak"`` to given ``Anna``, middle ``z``, family ``Nowak`` -- which is how it reads on the default vocabulary, unchanged from 1.4.0. The same applies to any other field-splitting workaround for this one field. A phrase is matched over whole tokens that stand together -- a bracketed clause or a comma between the words ends the run, so ``"Anna z (domu) Nowak"`` keeps its family name too -- and longest first, so a phrase and a word entry that starts it can both be configured: with ``geb`` shipped and ``geb von`` added, ``"Jane Smith geb von Braun"`` reads maiden ``Braun`` and ``"Jane Smith geb Braun"`` still reads maiden ``Braun``. ``given_name_titles`` was the only field exempt from the multi-word warning; ``maiden_markers`` is now the second, and the warning still fires for every other field. This addition takes the default marker set to 17 entries. Two differential corpus names move, at all three baselines, and both are examples this change adds to the rules doc; no corpus name held ``domu`` or a bare ``z`` before it (#434) + - Add the Polish maiden marker ``z domu`` to the default vocabulary, and let a ``maiden_markers`` entry be more than one word. ``"Maria Kowalska z domu Nowak"`` now reads family ``Kowalska``, maiden ``Nowak``, where every earlier version read the marker as part of the name (1.4.0: middle ``Kowalska z domu``, family ``Nowak``). The bracketed spelling moves with it, while a clause holding nothing but the marker stays a nickname (``"Maria Kowalska (z domu)"``). **If you worked around the old limit by storing ``z`` and ``domu`` as separate entries, remove both** -- a bare preposition is then a marker, and a marker takes every word after it, so ``"Anna z Nowak"`` loses its surname. ``maiden_markers`` and ``given_name_titles`` are now the two fields exempt from the multi-word warning. See :doc:`customize` (#434) - - Fix a bracketed maiden clause reading as a nickname because its brackets were not declared. ``"Jane Smith nee Jones"`` gave maiden ``Jones``, while ``"Jane Smith (née Jones)"`` gave nickname ``née Jones`` -- the parenthesis pair is a nickname delimiter by default and nothing looked inside it, so the marker word the writer put there was read as part of an alias. This is about every delimiter pair the parser ships, not only the parentheses these examples use: quotes are how a nickname is most often written, and ``"Jane Smith "née Jones""`` and ``"Jane Smith 'née Jones'"`` move from nickname to maiden exactly as the parenthesized spelling does. A bracketed clause whose content opens with a recognized marker word and carries a word after it now reads as the maiden name whichever bucket its pair sits in: ``"Jane Smith (née Jones)"`` reads family ``Smith``, maiden ``Jones``, and so does the Japanese ``"山田 花子(旧姓 佐藤)"`` (family 山田, given 花子, maiden 佐藤), which needed ``Policy(maiden_delimiters=...)`` through 2.1. The clause is lifted out as a region rather than having its brackets erased, which is what keeps an INTERIOR clause from eating the name behind it: ``"Jane (née Jones) Smith"`` reads given ``Jane``, family ``Smith``, maiden ``Jones``, where erasing the brackets would leave the bare ``"Jane née Jones Smith"``, whose marker takes every word to the end of the name -- maiden ``Jones Smith``, with the real surname inside that value and the given name standing in ``family`` (it read no family at all until #445, below). The closing bracket is a boundary the writer typed, and it is now where the maiden name stops. Two clauses beside each other no longer merge: ``"Jane "Janey" Smith (née Jones)"`` read nickname ``Janey née Jones`` through 2.1 and now reads nickname ``Janey``, maiden ``Jones``, each clause keeping its own role, and this reaches ``HumanName``. What does NOT move is a clause that says nothing about maiden names. A lone marker in brackets is just a word in brackets -- ``"Jane Smith (née)"`` stays nickname ``née``, as 1.4.0 read it -- because a marker with no name after it marks nothing, and because a one-word clause may be the surname it looks like (``Nee`` is Irish Ní/Nee and a Chinese romanization). A markerless clause stays a nickname too: ``"Cherice J. (Johnson) Williams"`` keeps nickname ``Johnson``, which is what ``Policy(maiden_delimiters=...)`` remains for -- the parenthesized birth surname is a real convention, and only a caller who knows the data can say that is what these brackets mean. One accepted consequence: the word after the marker is not tested for being a name word, so a bracketed clause does not stop at a credential where the bare form does -- ``"Jane Smith (née V)"`` reads maiden ``V`` while ``"Jane Smith née V"`` reads suffix ``V``. The clause goes to ``maiden`` whole and the bracket is its boundary, so the trailing-suffix rule never sees the word as trailing the name. One more shape moves with it, and it is the one to know about if you parse names that are a surname and a clause: a name that is ONLY a marker clause and one name word. A marker-led clause is not a nickname clause, so the rule that reads a nickname plus one name word as a surname no longer sees this shape -- and what names the family instead is the maiden marker itself, which is #445 below, in this same release. ``"Smith (née Jones)"`` reads family ``Smith``, maiden ``Jones``, where 1.4.0, 2.0.0 and 2.1.0 all read family ``Smith``, nickname ``née Jones``: the clause changes hands and the family name stays put. It is also the bracketed form agreeing with the bare ``"Smith née Jones"``, which #445 moves to family ``Smith`` from the given name every version through 2.1 read there. Without that rule this change would have left both spellings with an empty ``family``. Seven of the 1,085 differential corpus names move against the 2.1.0 and 2.0.0 baselines and six against 1.4.0: the Japanese name is the odd one, and against 1.4.0 it is not a new diff at all, since 1.4 read the fullwidth brackets as name text and had no nickname there to change hands. Eight corpus names have the shape and seven move (measured); the eighth is ``"Jane Smith (née Jr.)"``, which the suffix rule takes first. So the count says how few bracketed marker clauses the corpora hold rather than how narrow the change is -- six of the seven are hand-written rule or issue examples, only the Japanese name arriving from elsewhere, and the coverage that bounds the behavior is ``tests/v2/cases.py`` (closes #335) + - Fix a bracketed maiden clause reading as a nickname because its brackets were not declared. ``"Jane Smith nee Jones"`` gave maiden ``Jones`` while ``"Jane Smith (née Jones)"`` gave nickname ``née Jones``; the bracketed spelling now reads family ``Smith``, maiden ``Jones`` too, and so does the Japanese ``"山田 花子(旧姓 佐藤)"``, which needed ``Policy(maiden_delimiters=...)`` through 2.1. Every delimiter pair the parser ships moves the same way, quotes included. An interior clause no longer eats the name behind it (``"Jane (née Jones) Smith"`` keeps family ``Smith``), and two clauses beside each other each keep their own role (``"Jane "Janey" Smith (née Jones)"`` reads nickname ``Janey``, maiden ``Jones``). What does NOT move: a lone marker in brackets is just a word, so ``"Jane Smith (née)"`` stays a nickname, and a markerless clause stays a nickname too -- which is what ``Policy(maiden_delimiters=...)`` remains for. This reaches ``HumanName`` (closes #335) - - Fix a particle chain and a maiden name taking a trailing generational numeral as a name word: ``"John van der Berg V"`` read family ``van der Berg V`` and ``"John née Jones Smith V"`` read maiden ``Jones Smith V``, where ``"John Smith V"`` reads suffix ``V``. Both stops asked "is this a suffix?" with the test that vetoes a bare ``V`` as an initial -- the same question the bound given-name join asked until #401 -- and now ask the suffix rule's own walk, over the name as they would leave it: family ``van der Berg``, maiden ``Jones Smith``, suffix ``V``, for ``I`` and ``X`` alike; ``"John van der J. V"`` and ``"J. née Jones Smith V"`` keep their readings, the word before the numeral being an initial as written or as left. The chain stops before a bare credential with words to spare too, as ``"John Smith Ma"`` does, where the suffix rule will still find them once the chain has run: ``"John van der Berg Ma"`` reads suffix ``Ma``, which is how 1.4.0 read it, while ``"Freiherr von Berg MA"`` keeps family ``von Berg MA``; a maiden name keeps such a credential, since the words the marker takes are the very words that made it one. A particle that is also suffix vocabulary now ends the chain where it opens the trailing run: ``"John Smith Mc V"`` reads suffix ``Mc, V``. The chain also no longer swallows the given name behind an unlisted abbreviation: ``"Xyz. van Johnson"`` and ``"Esq. van Gogh"`` read given ``van``, as ``"Dr. van Johnson"`` has since #367, the chain and the bound given-name join now asking the same test that reads the abbreviation as a title (closes #424) + - Fix a particle chain and a maiden name taking a trailing generational numeral as a name word. ``"John van der Berg V"`` read family ``van der Berg V`` and ``"John née Jones Smith V"`` read maiden ``Jones Smith V``, where ``"John Smith V"`` reads suffix ``V``; both now stop before the numeral, for ``I`` and ``X`` alike. A word before the numeral that is an initial keeps its reading (``"John van der J. V"``). The chain also stops before a bare credential with words to spare -- ``"John van der Berg Ma"`` reads suffix ``Ma``, as 1.4.0 did -- and no longer swallows the given name behind an unlisted abbreviation: ``"Xyz. van Johnson"`` and ``"Esq. van Gogh"`` read given ``van`` (closes #424) - - Fix a name losing its given/family split when a comma is followed only by an honorific: ``"John Smith, Mr."`` now gives given ``John``, family ``Smith``, title ``Mr.`` where it returned the whole of ``"John Smith"`` as the family name. The family-comma rule that the part before the comma is wholly the surname rests on the writer having marked where the surname ends, and a comma followed by nothing but titles marked no such thing -- ``"John Smith, Mr."`` is ``"Mr. John Smith"`` with the honorific moved to the end. A comma followed by an actual name still fixes the family (``"John Smith, Jones"`` keeps family ``John Smith``), a title followed by a postnominal is each read where it stands (``"Smith, Dr. Jr."`` is unchanged), and a single pre-comma piece has no split to keep, so ``"Smith, Dr."`` is unchanged. Because the pre-comma name is now read positionally, it also picks up the two things the merged read suppressed: the script-order table, the declared name order (``"de Mesnil Juan, Dr."`` keeps family ``de Mesnil`` under a family-first order) and the particle-or-given ambiguity report (``"Van Johnson, Mr."``). The same holds when the comma is followed by titles and postnominals with no name word between them: ``"John Smith, Mr. Jr."`` keeps its split, while ``"Smith Jr., Mr."`` -- one name word -- keeps its family + - Fix a name losing its given/family split when a comma is followed only by an honorific. ``"John Smith, Mr."`` returned the whole of ``"John Smith"`` as the family name and now gives given ``John``, family ``Smith``, title ``Mr.``: it is ``"Mr. John Smith"`` with the honorific moved to the end, and marks no surname boundary. A comma followed by an actual name still fixes the family (``"John Smith, Jones"``), and a single pre-comma piece has no split to keep (``"Smith, Dr."`` is unchanged). The pre-comma name now also picks up the declared name order -- ``"de Mesnil Juan, Dr."`` keeps family ``de Mesnil`` under a family-first order -- and the particle-or-given ambiguity report (``"Van Johnson, Mr."``) - - Fix pure postnominals being claimed as titles: ``jr``, ``junior``, ``phd``, ``do`` and ``se`` have left the default ``titles`` vocabulary, and ``dr``/``sra`` have left the suffix vocabulary they never belonged in. v1's word lists recorded where the v1 parser needed a word to be in order to do its job, not where the word can actually occur -- ``"Smith, PhD"`` returned title ``PhD`` because the title peel legitimately got there first. Each of the nineteen words in the ``titles``/suffix overlap was reviewed and given its true positional class; twelve are genuine duals and keep both memberships, with position deciding (``"Lt. Smith"`` is a title, ``"Smith, LT"`` a postnominal; ``"Ms. Smith"`` is the honorific, ``"John Smith, MS"`` the degree -- and so, after a family comma, is ``"Smith, Ms."``, the slot being postnominal). ``md`` is one of them: bare ``Md`` before a name is the Bengali and South Asian abbreviation of Muhammad (``"Md Abdul Karim"``), ``MD`` after it the degree. The cost is in leading position, where a dropped word now reads as a name: ``"PhD Smith"`` gives given ``PhD`` where 1.x gave title ``PhD``. That is the correct reading of a string nobody writes, and it is what makes ``"Do Nguyen"`` parse as the Vietnamese name it is -- and ``"Dr. Do Van Johnson"`` read given ``Do``. ``dr`` and ``sra`` are the only two words that lose *suffix* membership, so they also stop being recognized in trailing position: ``"John Smith Dr."`` now gives family ``Dr.`` rather than suffix ``Dr.``. That is the same reading every other title-only word already gets there -- ``"John Smith Prof."`` and ``"John Smith Mr."`` have always parsed this way, and the suffix entry was the only thing making ``dr`` an exception. Routing a trailing title word to ``title``, which the comma path already does and the no-comma path does not, is a separate open question (#316). An ambiguous credential acronym (``ma``, ``ed``, ``jd``, ``do``) now counts as a suffix only when written with its periods, one after each letter -- ``"John Smith M.A."`` -- and no longer on any period: ``"Jack Ma."`` keeps family ``Ma.``, as 1.4.0 read it, where 2.0 read a suffix + - Fix pure postnominals being claimed as titles: ``jr``, ``junior``, ``phd``, ``do`` and ``se`` have left the default ``titles`` vocabulary, and ``dr``/``sra`` have left the suffix vocabulary they never belonged in, so ``"Smith, PhD"`` gives suffix rather than title ``PhD``. All nineteen words in the ``titles``/suffix overlap were given a positional class; twelve are genuine duals and keep both memberships, with position deciding -- ``"Lt. Smith"`` is a title, ``"Smith, LT"`` a postnominal, bare ``Md`` before a name the Bengali and South Asian abbreviation of Muhammad, ``MD`` after it the degree. The cost is in leading position, where a dropped word now reads as a name: ``"PhD Smith"`` gives given ``PhD``, which is what makes ``"Do Nguyen"`` parse as the Vietnamese name it is. ``dr`` and ``sra`` also stop being recognized in trailing position, so ``"John Smith Dr."`` gives family ``Dr.`` as ``"John Smith Prof."`` always has. An ambiguous credential acronym (``ma``, ``ed``, ``jd``, ``do``) counts as a suffix only when written with its periods, so ``"Jack Ma."`` keeps family ``Ma.`` as 1.4.0 read it. Routing a trailing title word to ``title`` is a separate open question (#316) - - Fix a credential run after a one-word family comma reading as a title or a given name: ``"Smith, Jr."`` and ``"Smith, PhD"`` now give suffix ``Jr.``/``PhD`` where they gave title, and ``"Smith, Ph. D. Jr."`` gives suffix ``Ph. D. Jr.`` where the split credential landed in the given name (a regression from 1.4.0, which read suffix ``Ph. D.``). The position right after a family comma is postnominal position, so a run that is nothing but suffix words is read as the credential run it is before the title peel's whole-segment exception can claim it. Vocabulary still decides which words qualify -- ``"Smith, Dr."`` keeps title ``Dr.``, because ``dr`` is not postnominal vocabulary at all -- and the leading readings are untouched (``"Sr. Garcia"`` is still title ``Sr.`` while ``"Smith, Sr."`` is now suffix ``Sr.``); a name word in the run makes it the given-and-suffix reading it always had (``"Smith, John Jr."``) (closes #296, closes #325) - - Fix a space-separated credential run after a family comma rendering with a comma the name never had: ``"Smith, MD PhD"`` gives suffix ``MD PhD`` where it gave ``MD, PhD``, and ``"Smith, CBE MC"``, ``"Smith, BSc MBA"``, ``"Smith, PhD Jr."`` and ``"Smith, Dr. MD PhD"`` the same. The roles are unchanged; only the rendered string carried the extra comma. One comma segment is one suffix entry, and grouping decided which segments those were by their position while assignment decided the same segment by its content, so the two disagreed about the segment a family comma leaves after it. The reading follows assignment now, which is where the segment's roles were already decided. This reaches any family comma whose following segment holds no name word, not only a one-word family: ``"John Smith, Jr. III"`` gives suffix ``Jr. III``, which is also what 1.4.0 gave. The full-name comma form was never affected -- ``"John Smith, MD PhD"`` has given suffix ``MD PhD`` since 1.4.0 -- and this brings the rest into line with it. A run written with commas keeps them (``"Smith, MD, PhD"`` is still suffix ``MD, PhD``), a comma between two entries survives (``"Smith Jr., Mr. Jr."`` is still suffix ``Jr., Jr.``), and a name word in the segment still makes it the given-and-suffix reading (``"Smith, John Jr."``). A name written without a comma is unaffected and still renders its run comma-joined, so re-parsing ``str()`` output does not reproduce the run (closes #429) - - Fix a one-character suffix word after a comma being read by the wrong neighbour: ``"Smith, PSM I"`` gives suffix ``PSM I`` where it gave given ``PSM`` and suffix ``I``, and ``"Smith, John V."`` gives middle ``V.`` where it gave suffix ``V.``. Inside a comma part a suffix word short enough to be mistaken for an initial -- ``I``, ``V`` and ``2`` in the shipped vocabulary -- is read by what stands before it. Behind a credential it is describing that credential -- ``PSM I`` is Professional Scrum Master level I -- so the run continues through it whether or not a period is written, since an initial in that position is not a name shape anyone writes; ``"Smith, MD I"`` and ``"Smith, Jr. I"`` reached the same wrong answer by a different route and move with it. Behind a name the period decides, marking an abbreviation and so a middle initial, exactly as ``"Smith, John B."`` always read. A numeral written bare after a name is still the generation it looks like (``"Smith, John V"`` is suffix ``V``), and a name with no comma is untouched (closes #430, closes #432) + - Fix a credential run after a one-word family comma reading as a title or a given name. ``"Smith, Jr."`` and ``"Smith, PhD"`` now give suffix ``Jr.``/``PhD`` where they gave title, and ``"Smith, Ph. D. Jr."`` gives suffix ``Ph. D. Jr.`` where the split credential landed in the given name -- a regression from 1.4.0. The position right after a family comma is postnominal position. Vocabulary still decides which words qualify (``"Smith, Dr."`` keeps title ``Dr.``), the leading readings are untouched (``"Sr. Garcia"`` is still title ``Sr.``), and a name word in the run makes it the given-and-suffix reading it always had (``"Smith, John Jr."``) (closes #296, closes #325) - - Fix a name opening with a particle that is *never* a given name being split at the particle under a family-first name order -- ``Policy(name_order=FAMILY_FIRST)`` and ``Policy(name_order=FAMILY_FIRST_GIVEN_LAST)`` alike, and identically: ``"de Mesnil"`` read as family ``de``, given ``Mesnil``, and ``"de la Vega"`` as family ``de``, given ``la Vega``. Each is now the whole surname, as it has always been in the default order. The rule enforcing it asked for the particle by the ``GIVEN`` role, which under a family-first order belongs to the token *after* the particle, so the test read the wrong word and declined. It now also asks by position -- the piece that opens the name -- so both shapes of the same rule are caught: where such a particle stands alone as a piece OPENING the name, the given and the middles fold into the family. (The rule reached a particle standing in the GIVEN position too until #467 below, which gives that slot back to the declared name order.) Standing *alone* is the whole of it, and the rule claims nothing wider: ``"Juan de la Vega"`` under ``FAMILY_FIRST`` still reports given ``de la Vega``, because there the particle chained onto the words after it rather than standing alone, and a bare ``"de"`` with nothing to fold into is still reported as the given name. The decision behind the fix: a word that can never be a given name leaves ``name_order`` nothing to decide, so declaring family-first is not a reason to make ``de`` a surname on its own. A leading particle that *may* be a given name is genuinely order-dependent and is untouched -- ``"van Gogh"`` still reads as family ``van``, given ``Gogh`` under both family-first orders. This is also what gives ``Lexicon.particles_ambiguous`` an effect outside the default order: taking a word out of it now changes the parsed fields under a family-first order, where before it moved only the ambiguity report. Seven of the 751 differential corpus names move, the same seven under each family-first order; default-order output is byte-identical over all 751, at the 1.4.0, 2.0.0 and 2.1.0 differential baselines alike (closes #359) + - Fix a space-separated credential run after a family comma rendering with a comma the name never had. ``"Smith, MD PhD"`` gives suffix ``MD PhD`` where it gave ``MD, PhD``, and ``"Smith, CBE MC"``, ``"Smith, BSc MBA"`` and ``"Smith, Dr. MD PhD"`` the same. The roles are unchanged; only the rendered string carried the extra comma. This reaches any family comma whose following segment holds no name word, not only a one-word family, so ``"John Smith, Jr. III"`` gives suffix ``Jr. III`` -- also what 1.4.0 gave. A run written with commas keeps them (``"Smith, MD, PhD"``), and a name written without a comma is unaffected and still renders its run comma-joined, so re-parsing ``str()`` output does not reproduce the run (closes #429) - - Fix a family name made only of particle words reporting no base on the 2.0 API, so the surname vanished from ``family_base`` and, on both APIs, from the initials: ``parse("Anh Do")`` gave family ``Do`` with ``family_base`` ``''`` and initials ``A.``, and under ``Policy(name_order=FAMILY_FIRST)`` ``"Del Toro"`` gave family ``Del`` the same way. The empty base was a 2.0 regression rather than a longstanding bug -- 1.4.0's own guard kept ``HumanName("Anh Do").last_base`` at ``Do``, and the facade has kept it right throughout; what 1.4.0 and 2.1 shared was the missing initial. A particle earns its name by joining forward to the word it modifies, so a particle standing alone in a name part is not doing a particle's work there and reads as an ordinary name word: it anchors the base, leaves the particles view, and contributes an initial. ``"Anh Do"`` is now base ``Do``, initials ``A. D.``; ``"Juan van der"`` is base ``van der``, initials ``J. v. d.``; ``"Nguyen, Van Le"`` initials ``V. L. N.`` where the middle name used to be dropped. Position decides this, not vocabulary -- whether the word is borne as a surname somewhere does not enter into it, which reverses the reasoning ``rules.md#R2`` carried before. The invariant it exists to hold: a non-empty family always has a non-empty base, because a particle needs a base to attach to. Where the particles DO join a name word nothing changes -- ``"Juan de la Vega"`` keeps base ``Vega``, particles ``de la`` and initials ``J. V.``. The parse fields themselves do not move: ``title``, ``given``, ``middle``, ``family``, ``suffix``, ``nickname`` and ``maiden`` are byte-identical over all 751 differential corpus names in all three name orders, and only the derived views and the initials change -- which is why the differential harness, which compares those seven roles, reports no diffs for this at any baseline. One consequence to know about: where the whole family is particles AND a tussenvoegsel was attached to it (#379), this change exposed a pre-existing ordering gap between ``initials()`` and the family field of the same name -- fixed in the #408 entry below, so the two agree as shipped (closes #385, closes #402) + - Fix a one-character suffix word after a comma being read by the wrong neighbour. ``"Smith, PSM I"`` gives suffix ``PSM I`` where it gave given ``PSM`` and suffix ``I``, and ``"Smith, John V."`` gives middle ``V.`` where it gave suffix ``V.``. Inside a comma part a suffix word short enough to be mistaken for an initial -- ``I``, ``V`` and ``2`` in the shipped vocabulary -- is read by what stands before it: behind a credential it describes that credential (``PSM I`` is Professional Scrum Master level I), and behind a name a period marks an abbreviation and so a middle initial. A numeral written bare after a name is still the generation it looks like (``"Smith, John V"`` is suffix ``V``), and a name with no comma is untouched (closes #430, closes #432) - - Fix case repair lowercasing the words of a family name made only of particle words, where every other view already reads them as ordinary name words: ``HumanName("ANH DO").capitalize()`` gives ``Anh Do`` where it gave ``Anh do``, and ``"anh van do"`` gives ``Anh Van Do`` where it gave ``Anh van do``. A particle earns its name by joining forward to the word it modifies, so a part whose every word is particle vocabulary leaves none of them anything to join; the fix above already made those words anchor ``family_base`` and contribute initials, and case repair now agrees with them rather than reading the same word two ways. The test is the whole part, not a particle standing alone, which is why the two-word family in ``"anh van do"`` moves along with the one-word family in ``"ANH DO"`` -- the same Vietnamese surname, and a standing-alone test would have read it one way behind a given name and another way alone. This DIFFERS FROM 1.4.0 deliberately and does not restore it: 1.4.0 returned ``Anh do``, lowercasing on vocabulary membership alone. The accepted cost is that a degenerate family which is nothing but particles capitalizes too, so ``"juan van der"`` gives ``Juan Van Der`` where 1.4.0 gave ``Juan van der``. A conjunction is untouched by any of this, so ``"der, y van"`` gives ``y Van Der`` -- the family capitalizing while the conjunction keeps the lowercase it always had; and where the particles DO join a name word nothing changes, ``"juan de la vega"`` still giving ``Juan de la Vega``. Case repair is not one of the seven role fields the differential harness compares, so no gate run can see this change either way and none of its counts move; the ``rules.md#R4`` examples and the v1 capitalization tests are what pin it (closes #407) + - Fix a name opening with a particle that is *never* a given name being split at the particle under a family-first name order. ``"de Mesnil"`` read as family ``de``, given ``Mesnil`` and ``"de la Vega"`` as family ``de``, given ``la Vega``; each is now the whole surname, as it has always been in the default order, under ``FAMILY_FIRST`` and ``FAMILY_FIRST_GIVEN_LAST`` alike. A word that can never be a given name leaves ``name_order`` nothing to decide. Standing alone is the whole of it: ``"Juan de la Vega"`` under ``FAMILY_FIRST`` still reports given ``de la Vega``. A leading particle that *may* be a given name is genuinely order-dependent and is untouched, so ``"van Gogh"`` still reads family ``van``, given ``Gogh`` under both family-first orders (closes #359) - - Change case repair to read the parser's own ``conjunction`` tag instead of re-deciding, from the word's spelling, whether a word is a conjunction or an initial. The parse answers that question already -- ``"Scott E. Werner"`` reads ``E.`` as an initial rather than the Italian conjunction -- and the other views honor the answer; case repair asked again, with a shape test applied to each word of a token's text rather than to the token. Two spellings of one name disagreed because of it: ``"juan e-f smith"`` capitalized to ``Juan e-F Smith`` while ``"JUAN E-F SMITH"`` gave ``Juan E-F Smith``; both give ``Juan E-F Smith`` now, ``e-f`` being a middle name and no conjunction of the parse's reading. A conjunction written as a word of its own is untouched, and so is the one-letter carve-out where it applies -- ``"juan y garcia"`` still repairs to ``Juan y Garcia``, ``"JUAN Y GARCIA"`` still to ``Juan Y Garcia``. A field assigned after the parse is unaffected: its text was never classified, so there is no reading to honor and repair asks the vocabulary, applying v1's own predicate the way every earlier version applied it everywhere -- ``h.last = "velasquez y garcia"`` still repairs to ``Velasquez y Garcia`` and ``h.middle = "e."`` to ``E.``. That is the predicate over TODAY's vocabulary, which is narrower than parity with 1.4.0 and the difference is real: ``h.last = "хосе и мария сантос"`` gives ``Хосе И Мария Сантос`` on 1.4.0 and ``Хосе и Мария Сантос`` here, because the Cyrillic ``и`` is a 2.x conjunction and was not a 1.4.0 one. What decides which path a token takes is a mark the assignment leaves, not the absence of a span: a value revised through ``Parser.revise()`` is classified by a sub-parse and keeps its tags, so it repairs as the parse does. One reading does change for hand-built ``Token``\ s in the 2.0 API: an untagged token whose text is conjunction vocabulary is now an ordinary name word and capitalizes, where 2.1 lowercased it -- tags are what the views read, and a hand-built token that carries none is a token with nothing to declare. Case repair is not one of the seven role fields the differential harness compares, so no gate run can see this change either way and none of its counts move; measured directly instead, no name of the 1094-name differential corpus moves under ``capitalized()`` or ``capitalized(force=True)``, its uppercased and lowercased spellings included -- 6564 name/spelling/lexicon rows and 13128 calls (closes #458) + - Fix a family name made only of particle words reporting no base, so the surname vanished from ``family_base`` and from the initials. ``parse("Anh Do")`` gave family ``Do`` with ``family_base`` ``''`` and initials ``A.``, and under ``Policy(name_order=FAMILY_FIRST)`` ``"Del Toro"`` gave family ``Del`` the same way. A particle standing alone in a name part is not doing a particle's work there and now reads as an ordinary name word: ``"Anh Do"`` is base ``Do``, initials ``A. D.``; ``"Juan van der"`` is base ``van der``, initials ``J. v. d.``; ``"Nguyen, Van Le"`` initials ``V. L. N.`` where the middle name used to be dropped. Where the particles DO join a name word nothing changes (``"Juan de la Vega"`` keeps base ``Vega``). The parse fields themselves do not move -- only the derived views and the initials. See the ``R2`` entry of ``docs/design/decisions.md`` (closes #385, closes #402) - - Change the parse-cost benchmark to bound FUNCTION CALLS per parse rather than wall-clock seconds. The two ``under_a_second`` tests asserted that 1000 parses take under a second; on CI that bound failed four times across two branches at 1.01 to 1.08 seconds while the same code re-ran green on master, and three local measurement methods disagreed with CI and with each other. Frame counts do not move under load, so growth shows up in a diff instead of in a threshold that happens to break. The bound is a per-interpreter band of ±2% -- per interpreter because the count is NOT machine-independent (410 calls for ``parse()`` on 3.11, 388 on 3.12, 406 on 3.13 and later, PEP 709 having inlined the comprehension frames 3.11 counts) -- and a loose five-second backstop stays over both entry points for the class frame counts cannot see, such as a regex that starts backtracking. What the old bound had caught is recorded rather than lost: measured on Python 3.11, 2.2 costs 67 more calls per parse than 2.1.0, of which #424's mirrored peel walks are 23; #439's predicate extraction, which an earlier draft of this note blamed, costs zero. Every figure is recomputable with ``uv run python tools/perf/call_count.py --against v2.1.0``, and the reasoning is in the ``parse-cost`` entry of ``docs/design/decisions.md``; the counts are a dated snapshot, measured 2026-08-31, since nothing in the repository re-runs them. No user-visible behavior changes (closes #475) - - Fix a name that opens with a spaced ``Ph. D.`` losing its surname. ``parse("Ph. D. Van Johnson")`` read given ``Van Johnson`` with an empty ``family`` and suffix ``Ph. D.``; it now reads title ``Ph.``, given ``D.``, family ``Van Johnson``. A suffix never begins a name -- position outranks the vocabulary match -- and the merge that heals a split ``Ph.``/``D.`` into one credential is what made a leading credential possible at all: every other suffix-shaped word standing first already falls out as a title (``Jr.``, ``MD``, ``Esq.``, ``Sr.``) or as an ordinary name word (``PhD``, ``III``), so this pair was the only shape that reached the defect. The merge is unchanged everywhere else, and a family comma still opens a listing rather than a name, so ``"John Smith Ph. D."`` keeps suffix ``Ph. D.`` and ``"Smith, Ph. D. Jr."`` keeps suffix ``Ph. D. Jr.``. This RESTORES 1.4.0, whose own healing regex required a preceding space and so could never fire at the head of the string -- measured on the released wheel, three of the four corpus names of this shape return to their v1 reading exactly, and the 1.4.0 ledger loses the rule that used to excuse the difference. The fourth, ``"Ph. D., Jr."``, still differs in where the ``D.`` lands and rides under a pre-comma rule that predates this change. What "the head" means is the head of the STRING, not of the name: a title before the credential keeps it a credential, so ``"Sir Ph. D. Van Johnson"`` still reads given ``Van Johnson`` with no family -- also 1.4.0's reading, and recorded as a boundary in ``rules.md#S2`` rather than left implied. One accepted consequence: ``Parser.revise(suffix="Ph. D.")`` renders ``Ph., D.``, since revise() sub-parses the string it is given and a field value has no head for a head-position rule to read (closes #371) - - Fix a trailing surname particle being stranded as a standalone middle name under ``Policy(name_order=FAMILY_FIRST)``, where the same listing written with a comma reads it as part of the surname. A particle ending the name has nothing to link forward to, so what it is doing there is decided by what the writing says: after a family comma it joins the family the comma named and is written before it, and a declared family-first order names the family the same way. ``Parser(policy=Policy(name_order=FAMILY_FIRST)).parse("Jong Anke de")`` gave family ``Jong`` with ``de`` left as a middle name, and now gives family ``de Jong``, given ``Anke`` -- the same answer ``parse("Jong, Anke de")`` has always given. The test is the SLOT the particle landed in, not the word: a middle name is a further given name, which a particle is not, and ``FAMILY_FIRST`` is the only order that puts a trailing piece there. ``FAMILY_FIRST_GIVEN_LAST`` puts it in the given slot, where the caller's own declaration says it is the given name, so ``"Nguyen Thi Van"`` under that order still reads given ``Van``. That one test reads both traditions without asking about the vocabulary at all: ``"Beethoven Ludwig van"`` under ``FAMILY_FIRST`` now gives family ``van Beethoven`` even though ``van`` is one of the 37 particles that are ordinary given names elsewhere. In the same change, a particle standing alone where a family-first order puts the GIVEN name is no longer folded into the family: ``"Ménil de"`` reports given ``de`` under both family-first orders, because that slot holds what the caller declared, and the never-given word list supplies a reading where position leaves the question open rather than overriding one position has already given. Nothing moves under the DEFAULT name order. Measured, 30 of 6606 parses move -- this release's 1101-name corpus under three ``name_order`` values with ``middle_as_family`` off and on -- over twelve names. Those counts cannot come from the differential gate: it parses every corpus name with the default policy and sweeps no policy at all, so no non-default ``name_order`` behavior has ever been compared across versions, and the gate output here is unchanged at all three baselines apart from the corpus names this change's own rules.md examples add. What moves is recomputed by the recipe in the ``P6`` entry of ``docs/design/decisions.md``, which compares the seven role fields against a checkout of the parent commit reading the same corpus files; the count is a dated snapshot, measured 2026-08-30. The ``rules.md#P1`` and ``rules.md#P6`` example lines and ``tests/v2/pipeline/test_post_rules.py`` are what pin the behavior (closes #467) - - Fix ``initials()`` reading a name in a different order than the fields of the same name. Two rules fold words into the family name and render them before the rest of it -- ``Policy(middle_as_family=True)``, which sends every middle word to the family, and the tussenvoegsel attachment after a family comma -- and both do it by marking the words rather than moving them, since a parsed word keeps the position it was written at. The ``family`` field reads that mark and ``initials()`` did not, so one parse gave two orders: ``parse("der, y van")`` gave family ``van der`` and initials ``y. d. v.``, and now gives ``y. v. d.``. This RESTORES v1: ``middle_name_as_last`` is v1's spelling of the same option, so most of what moves has a 1.4.0 answer to be measured against, and measured over the 1094-name differential corpus at the default name order, 71 names move under that option, 54 of them back to exactly what 1.4.0 returns and not one of them away from it -- ``"Doe, Dr. John A."`` gives ``J. A. D.`` again where 2.0 through 2.2 gave ``J. D. A.``, and ``"Brundridge, Contessa A"`` gives ``C. A. B.`` where they gave ``C. B. A.``. Of the 17 that match 1.4.0 neither before nor after, 14 now agree with it on the ORDER and differ only in how v1 grouped initials -- 1.4.0 gives one initial per element of its own ``last_list``, so a conjunction-joined surname yields ``V G.`` where the 2.x view, one initial per word, yields ``V. G.``; one more is ``"der, y van"``, whose family is nothing but particles, where 1.4.0 contributes no initial at all and 2.x contributes its words, a difference this release does not touch; and the remaining two parse differently from 1.4.0 for reasons that predate this fix. Without the option, one corpus name moves, the ``"der, y van"`` above -- and it is one of one, since it is the only name in the corpus whose family holds two contributing words with a folded one behind the other, the shape an order change can be seen in at all. ``HumanName.initials()`` was already right and is unchanged, measured: no name of the 1094-name corpus moves through the facade, with the option or without it. It reads ``first_list``/``middle_list``/``last_list``, which prepend the folded words as v1 did, so it is the 2.0 API's ``ParsedName.initials()`` that was out of step -- with the field beside it, with the facade, and with 1.4.0 at once. ``initials()`` is not one of the seven role fields the differential harness compares, so no gate run can see this change: run at all three baselines before and after, the output is identical to the byte -- 1094 corpus names, 229 / 194 / 102 intentional diffs and ``unexplained: 0`` at 1.4.0 / 2.0.0 / 2.1.0, with every per-heading count unchanged. So where the counts in this bullet come from has to be said, the gate's classified summary not being able to supply them and the ``rules.md#R3`` example line witnessing the order without counting anything: what MOVES is recomputed by the recipe in the ``R3`` entry of ``docs/design/decisions.md``, which compares this view against the pre-change rendering over these same four corpora and reproduces the 71 and the one-of-one; the 1.4.0 comparisons and the facade sweep are dated snapshots rather than re-derivable ones, measured 2026-08-30 against the released 1.4.0 wheel and against the pre-change tree, which nothing in the repository re-runs. The ``rules.md#R3`` example line and ``tests/v2/test_render.py`` are what pin the behavior (closes #408) + - Fix case repair lowercasing the words of a family name made only of particle words, where every other view already reads them as ordinary name words. ``HumanName("ANH DO").capitalize()`` gives ``Anh Do`` where it gave ``Anh do``, and ``"anh van do"`` gives ``Anh Van Do``. The test is the whole part, not a particle standing alone, which is why the two-word family moves along with the one-word one. This DIFFERS FROM 1.4.0 deliberately and does not restore it: 1.4.0 returned ``Anh do``. The accepted cost is that a family which is nothing but particles capitalizes too, so ``"juan van der"`` gives ``Juan Van Der``. A conjunction is untouched (``"der, y van"`` gives ``y Van Der``), and where the particles DO join a name word nothing changes (``"juan de la vega"`` still gives ``Juan de la Vega``) (closes #407) - - Fix a tussenvoegsel attached to the family name after a comma deciding a genuinely uncertain reading and reporting nothing. ``"Van Johnson"`` reports a ``PARTICLE_OR_GIVEN`` ambiguity -- ``Van`` is a Dutch particle and a Vietnamese given name, and the parser has to pick one -- while ``"Nguyen, Thi Van"`` picked the same word the same way, silently, and lost the given name doing it. The attachment now reports the fork it decides, in the kind that names the reading it declined. A particle that could be an ordinary name reports ``PARTICLE_OR_GIVEN``: ``"Nguyen, Thi Van"``, ``"Berg, Jan van der"`` and ``"Vega, Juan de la"`` each gain one, the ``detail`` naming the ambiguous word. A particle the parser had already read as a post-nominal reports ``SUFFIX_OR_NAME`` instead, because the credential reading is what the attachment overrode: ``"Berg, Jan vd"`` gains one, ``vd`` being read as *van der* rather than as the Volunteer Decoration. Which kind you get follows the reading that was overridden rather than the word's vocabulary, so ``"Berg, Jan do"`` reports ``PARTICLE_OR_GIVEN`` even though ``do`` is a postnominal too -- it was already being read as a name word, so no credential reading was overridden. A particle where nothing was overridden reports nothing at all: ``"Jong, Piet de"`` and ``"Jong, Anke de"`` are unchanged, ``de`` being no name in any reading and no postnominal either, and so is ``"Berg, Jan de vd"``, whose run was read as name words whole. Worth knowing before you filter on this: ``"Beethoven, Ludwig van"`` -- the textbook Dutch listing, read exactly right -- now carries a report too. It is the same string shape over the same vocabulary as ``"Nguyen, Thi Van"``, and nothing in the input separates them, so a report on one is a report on both. This adds the report and nothing else: every field these names parse to is exactly what the tussenvoegsel fix below already gave them, and ``ambiguities`` is the only value that grows. Seven differential corpus names gain a kind against the 2.0.0 and 2.1.0 baselines, seven of the nine the tussenvoegsel fix below already moved -- five of ``fix(#379)``'s seven and both of ``fix(#380)``'s two, now carrying ``_ambiguities`` in their diff as well; the two left out are the pair named unchanged above, ``Jong, Piet de`` and ``Jong, Anke de`` -- and none against 1.4.0, which had no ambiguity reporting at all (closes #405) + - Change case repair to read the parser's own ``conjunction`` tag instead of re-deciding, from the word's spelling, whether a word is a conjunction or an initial. Two spellings of one name disagreed because of it: ``"juan e-f smith"`` capitalized to ``Juan e-F Smith`` while ``"JUAN E-F SMITH"`` gave ``Juan E-F Smith``; both give ``Juan E-F Smith`` now. A conjunction written as a word of its own is untouched (``"juan y garcia"`` still repairs to ``Juan y Garcia``). A field assigned after the parse was never classified, so repair asks the vocabulary there -- today's vocabulary, which is narrower than 1.4.0 parity: ``h.last = "хосе и мария сантос"`` gives ``Хосе И Мария Сантос`` on 1.4.0 and ``Хосе и Мария Сантос`` here. One reading changes for hand-built ``Token``\ s in the 2.0 API: an untagged token whose text is conjunction vocabulary now capitalizes as an ordinary name word. See the ``R4`` entry of ``docs/design/decisions.md`` (closes #458) - - Fix a tussenvoegsel after a family comma being parsed as a middle name. Dutch and Belgian alphabetized listings move the particle behind the given name -- ``"Beethoven, Ludwig van"`` is how ``"Ludwig van Beethoven"`` is filed -- and the trailing particle run was read as a middle name rather than as part of the surname: ``"Beethoven, Ludwig van"`` gave middle ``van``, last ``Beethoven``, and ``"Berg, Jan van der"`` gave middle ``van der``. The run now attaches to the family the comma has already named and renders before it, so those read family ``van Beethoven`` and ``van der Berg`` with the given name unchanged. The derived views move with the parse, so ``family_particles`` is ``van`` and ``family_base`` is ``Beethoven`` where they were empty and ``Beethoven`` before. `#130 `_ asked for the split and got it in 1.3.0 as ``last_base``/``last_prefixes``; 2.0 renamed them ``family_base``/``family_particles``. What was wrong until now was the values they reported for this listing. Both halves of the particle vocabulary attach -- never-given ``de`` and may-be-given ``van`` alike -- because after a comma the family is already named and the particle has no other role to take. Two guards bound it. A name whose only given word is the particle keeps it, so ``"Nguyen, Van"`` still reads given ``Van``: the attachment needs a given word to spare. And where the word is BOTH particle and suffix vocabulary the attachment outranks the post-nominal reading, so ``"Berg, Jan vd"`` reads family ``vd Berg`` where 1.4.0 and 2.1 alike gave suffix ``vd`` -- a trailing abbreviation after a family comma is the tussenvoegsel far more often than the decoration it collides with, and the same shape sweeps in ``mc``, which 2.1 also read as a suffix. ``do`` is in ``SUFFIX_ACRONYMS_AMBIGUOUS`` and 2.1 already read a trailing one as a name word, so it attaches by the plain rule rather than by the override (closes #379, closes #380). Names without the comma are untouched: ``"Ludwig van Beethoven"`` already read family ``van Beethoven`` and is byte-identical. One of the 751 differential corpus names moves, ``"Vega, Juan de la"``, at the 1.4.0, 2.0.0 and 2.1.0 baselines alike + - Change the parse-cost benchmark to bound function calls per parse rather than wall-clock seconds. The two ``under_a_second`` tests asserted that 1000 parses take under a second; on CI that bound failed four times across two branches at 1.01 to 1.08 seconds while the same code re-ran green on master. Frame counts do not move under load, so growth shows up in a diff instead of in a threshold that happens to break. The bound is a per-interpreter band of ±2%, with a loose five-second backstop over both entry points for the class frame counts cannot see. Every figure is recomputable with ``uv run python tools/perf/call_count.py --against v2.1.0``; the counts and the per-PR attribution are in the ``parse-cost`` entry of ``docs/design/decisions.md``. No user-visible behavior changes (closes #475) - - Add ``abd`` to :data:`~nameparser.config.bound_given_names.BOUND_GIVEN_NAMES`, so the spellings that write the article as its own word join like the others do: ``"abd Allah Smith"`` was given ``abd``, middle ``Allah`` and is now given ``abd Allah``. ``abdul``, ``abdel`` and ``abdal`` were already there, and the Arabic-script ``عبد`` has covered the same word since 2.0, so only the Latin spelling was short. The word is also the postnominal ABD ("All But Dissertation") and stays in ``SUFFIX_ACRONYMS``: position tells the two readings apart, so ``"Jane Smith ABD"``, ``"Jane Smith, ABD"`` and ``"Jane Smith A.B.D."`` all still read the credential as a suffix. Making that work needed one fix beyond the wordlist. The join reserves enough words to leave a family name behind, and the reserve counted every piece except titles and suffixes -- so a word in BOTH vocabularies did not count as a name word even when the rule had already claimed it as one, and the join declined silently on the common three-word shape. The claimed piece now counts, which is what the reserve means: it asks whether enough OTHER words are left to spare. No shipped word other than ``abd`` is in both sets, and for any word that is not, the changed expression reduces to the old one -- so nothing else can move, by arithmetic rather than by sampling. The differential corpus agrees at all 751 names, but it holds no name carrying a dual-membership word, so that agreement tests the corpus rather than the change (#400) + - Fix a name that opens with a spaced ``Ph. D.`` losing its surname. ``parse("Ph. D. Van Johnson")`` read given ``Van Johnson`` with an empty ``family`` and suffix ``Ph. D.``; it now reads title ``Ph.``, given ``D.``, family ``Van Johnson``. A suffix never begins a name, and every other suffix-shaped word standing first already fell out as a title (``Jr.``, ``MD``, ``Esq.``) or as an ordinary name word (``PhD``, ``III``), so the split credential was the only shape that reached the defect. A family comma still opens a listing rather than a name (``"John Smith Ph. D."`` and ``"Smith, Ph. D. Jr."`` keep their suffixes), and "the head" means the head of the string rather than of the name, so ``"Sir Ph. D. Van Johnson"`` is unchanged. This RESTORES 1.4.0. One accepted consequence: ``Parser.revise(suffix="Ph. D.")`` renders ``Ph., D.`` (closes #371) - - Change how far a leading never-given particle takes the surname when a family-first ``name_order`` is declared. ``Policy(name_order=FAMILY_FIRST)`` read ``"de Mesnil Juan"`` as family ``de Mesnil Juan`` -- the whole name -- and now reads family ``de Mesnil``, given ``Juan``. Declaring a family-first order asserts that what follows the family is not more surname, and where the surname run ends is exactly that question, so the declaration settles it. The default order is unchanged, deliberately: with no order declared nothing marks where the surname ends, and a particle followed by several words really can be all surname -- ``von Bergen Wessels`` is one such name. Nothing in ``"de Mesnil Juan"`` distinguishes it from that reading except a declared order or a comma. A caller who means family ``de la Vega`` plus given ``Juan`` in the default order writes the comma, which already parses that way. The run takes one name WORD rather than one token: a conjunction-joined run and a bound given-name pair each count once, so the stop cannot land inside one -- ``"de la Vega y Santos Juan"`` reads family ``de la Vega y Santos``, and ``"ibn Awf abdul Rahman"`` reads given ``abdul Rahman``. Where two or more words are left over the two family-first orders differ from each other for the first time: ``"de la Cruz Juan Carlos"`` reads given ``Juan``, middle ``Carlos`` under ``FAMILY_FIRST`` and middle ``Juan``, given ``Carlos`` under ``FAMILY_FIRST_GIVEN_LAST``. An ambiguous leading particle is untouched in every order -- ``"van Gogh Jan Pieter"`` still reads family ``van`` under both family-first orders -- and so is a family comma, where the comma has already fixed the surname (``"Smith, de Mesnil"`` keeps family ``Smith de Mesnil``). One of the 751 differential corpus names moves, ``"de Mesnil Garcia"`` to family ``de Mesnil``, given ``Garcia``, under each family-first order; default-order output is byte-identical over all 751. This reverses the answer #364 was closed on, and the reasoning is recorded at ``docs/design/decisions.md#P1`` (closes #395) + - Fix a trailing surname particle being stranded as a standalone middle name under ``Policy(name_order=FAMILY_FIRST)``, where the same listing written with a comma reads it as part of the surname. ``"Jong Anke de"`` gave family ``Jong`` with ``de`` left as a middle name and now gives family ``de Jong``, given ``Anke`` -- the answer ``parse("Jong, Anke de")`` has always given. The test is the SLOT the particle landed in, not the word, and ``FAMILY_FIRST`` is the only order that puts a trailing piece in a middle; ``FAMILY_FIRST_GIVEN_LAST`` puts it in the given slot, so ``"Nguyen Thi Van"`` under that order still reads given ``Van``. That reads both traditions without asking about the vocabulary: ``"Beethoven Ludwig van"`` under ``FAMILY_FIRST`` now gives family ``van Beethoven``. A particle standing alone in the given slot is no longer folded into the family either, so ``"Ménil de"`` reports given ``de``. Nothing moves under the DEFAULT name order. See the ``P6`` entry of ``docs/design/decisions.md`` (closes #467) - - Change the ``detail`` text of a ``PARTICLE_OR_GIVEN`` ambiguity to name the role the leading particle was actually given. It said "read as a given name" under every ``name_order``, which is false under ``Policy(name_order=FAMILY_FIRST)`` -- there ``"Van Johnson"`` reads as family ``Van``, given ``Johnson``, and the report described the reading not taken. It now ends "read as a family name" in that case, reading the role off the assigned token the way ``SUFFIX_OR_NAME`` already did -- that kind names both parts (``read as a family name rather than a post-nominal``), while this one names only the part it took. The ``kind`` is unchanged and stays ``PARTICLE_OR_GIVEN``: the fork really is particle-or-given, and only the human-readable text moved. Default-order output is identical (#355) + - Fix ``initials()`` ordering a name differently from the fields of the same parse. Two rules fold words into the family and render them ahead of it -- ``Policy(middle_as_family=True)`` and the tussenvoegsel attachment after a family comma -- and the ``family`` field honored the fold where ``initials()`` did not: ``parse("der, y van")`` gave family ``van der`` but initials ``y. d. v.``, and now gives ``y. v. d.``. Under ``middle_as_family`` this RESTORES v1, that option being ``middle_name_as_last``'s successor: ``"Doe, Dr. John A."`` gives ``J. A. D.`` again where 2.0 through 2.2 gave ``J. D. A.``. ``HumanName.initials()`` was already right and is unchanged. See the ``R3`` entry of ``docs/design/decisions.md`` (closes #408) - - Fix a maiden name being lost when a particle stood in front of the marker. ``"Ursula Leyen geb. Albrecht"`` reported maiden ``Albrecht`` correctly, but ``"Ursula von der Leyen geb. Albrecht"`` -- the same words one particle chain apart -- gave family ``von der Leyen geb. Albrecht`` and no maiden name at all, and ``"Jane van der Berg née Jones"`` failed the same way. The particle chain absorbed everything up to the next particle or suffix, and the marker is consumed later in the same grouping stage, by which point the chain had merged the marker away and there was no marker word left standing to find. A suffix already stopped the chain; a marker now does too, so those read family ``von der Leyen`` maiden ``Albrecht`` and family ``van der Berg`` maiden ``Jones``. Only a particle that is not the name's own first word ever reached the marker, which is why a single leading particle always worked -- ``"von Müller geb. Schmidt"`` was already right -- while a leading run of two did not: in ``"von der Müller geb. Schmidt"`` the second particle starts a chain of its own. The bound applies only where the marker is actually taken as a marker. A marker needs a word after it to hand the maiden name to, so a trailing one is just a word and bounds nothing: ``"Jane van der Berg née"`` and ``"Jane van der Berg née PhD"`` both keep family ``van der Berg née``, which is the reading ``"Jones née"`` -> family ``née`` already had. That distinction is what keeps a name like ``"Jane van der Nee"`` intact -- ``Nee`` is an attested surname as well as a marker spelling -- and what keeps ``"St St née"`` reporting its ``PARTICLE_OR_GIVEN`` fork. Two consequences fall out. Under a family-first order the marker used to survive as an ordinary name word and take the leftover given slot, so ``"de la Cruz née Vega"`` read given ``née``, middle ``Vega`` under ``Policy(name_order=FAMILY_FIRST)`` and given ``Vega``, middle ``née`` under ``Policy(name_order=FAMILY_FIRST_GIVEN_LAST)``; consumed and dropped it never reaches that placement, and both orders now read family ``de la Cruz``, maiden ``Vega``. And stopping the chain can leave a family of nothing but particles, which the #385 rule above already covers: ``"Jane de la née Jones"`` reports family ``de la`` with ``family_base`` ``de la`` rather than an empty base. Two limits remain, and both are recorded in ``rules.md#M2``: the bound reaches only a marker standing as a word of its own, so a conjunction join and a bound given-name join each still absorb one first -- ``"Jane van der Berg née y Jones"`` and ``"van der Berg, abdul née Jones"`` are unchanged. The bracketed form was never affected, since ``"Ursula von der Leyen (geb. Albrecht)"`` is extracted before grouping. When this landed no differential corpus name carried a particle ahead of a marker, so the class was invisible to the harness and the tests were the whole verification. The rules-doc corpus added in the same release closes that gap: three names of this shape are now compared at every baseline (closes #399) + - Fix a tussenvoegsel attached to the family name after a comma deciding a genuinely uncertain reading and reporting nothing. ``"Van Johnson"`` reports a ``PARTICLE_OR_GIVEN`` ambiguity -- ``Van`` is a Dutch particle and a Vietnamese given name -- while ``"Nguyen, Thi Van"`` picked the same word the same way, silently. The attachment now reports the fork it decides: ``"Nguyen, Thi Van"``, ``"Berg, Jan van der"`` and ``"Vega, Juan de la"`` each gain a ``PARTICLE_OR_GIVEN``, while a particle already read as a post-nominal reports ``SUFFIX_OR_NAME`` instead (``"Berg, Jan vd"``). A particle where nothing was overridden reports nothing, so ``"Jong, Piet de"`` is unchanged. Worth knowing before you filter on this: ``"Beethoven, Ludwig van"`` -- read exactly right -- now carries a report too, nothing in the input separating it from ``"Nguyen, Thi Van"``. ``ambiguities`` is the only value that grows (closes #405) - - 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) + - Fix a tussenvoegsel after a family comma being parsed as a middle name. Dutch and Belgian alphabetized listings move the particle behind the given name -- ``"Beethoven, Ludwig van"`` is how ``"Ludwig van Beethoven"`` is filed -- and it was read as a middle name rather than as part of the surname: ``"Beethoven, Ludwig van"`` gave middle ``van``, last ``Beethoven``, and ``"Berg, Jan van der"`` gave middle ``van der``. Those now read family ``van Beethoven`` and ``van der Berg``, and the derived views follow, so ``family_particles`` is ``van`` and ``family_base`` ``Beethoven``. Two guards bound it: a name whose only given word is the particle keeps it (``"Nguyen, Van"`` still reads given ``Van``), and where the word is BOTH particle and suffix vocabulary the attachment wins, so ``"Berg, Jan vd"`` reads family ``vd Berg`` where 1.4.0 and 2.1 alike gave suffix ``vd`` -- as does ``mc``. Names without the comma are untouched (closes #379, closes #380) - - 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 `_. 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) + - Add ``abd`` to :data:`~nameparser.config.bound_given_names.BOUND_GIVEN_NAMES`, so the spellings that write the article as its own word join like the others do: ``"abd Allah Smith"`` was given ``abd``, middle ``Allah`` and is now given ``abd Allah``. ``abdul``, ``abdel`` and ``abdal`` were already there, and the Arabic-script ``عبد`` has covered the same word since 2.0, so only the Latin spelling was short. The word is also the postnominal ABD ("All But Dissertation") and stays in ``SUFFIX_ACRONYMS``: position tells the two readings apart, so ``"Jane Smith ABD"``, ``"Jane Smith, ABD"`` and ``"Jane Smith A.B.D."`` all still read the credential as a suffix (#400) - - Fix a maiden clause changing how the rest of the name is read, and a connective join keeping the marker in the surname. A maiden marker and the name it introduces are not part of the name they follow, but the grouping rules that count a name's words -- the three-word test that keeps a single-letter connective a name word, and the bound given-name reserve -- counted them, because the pass that removes them ran after those rules. ``"juan y garcia"`` reads given ``juan``, middle ``y``, family ``garcia``, but ``"juan y garcia nee jones"`` counted five words, joined the ``y``, and read given ``juan y garcia`` with NO family name at all; ``"John e Smith nee jones"``, ``"Lt.Gov. juan e garcia nee jones"`` and six more differential corpus names lose the family the same way under an appended clause. The marker pass now runs before every join, so the joins and the counts see only the name that remains, and a name of two or more name words reads as it reads without its maiden clause, plus the maiden name: ``"juan y garcia nee jones"`` is given ``juan``, middle ``y``, family ``garcia``, maiden ``jones``. (A title plus ONE name word is the #410 shape, fixed separately below: ``"Dr. Jane née Smith"`` reads family ``Jane`` now, as ``"Dr. Jane"`` does.) The same order closes the last of the join-swallows. The connective join used to merge the marker into a multi-word piece before the marker rule could see it, so ``"Jane van der Berg née y Jones"`` kept family ``van der Berg née y Jones`` with no maiden name; it now reads family ``van der Berg``, maiden ``y Jones`` -- the marker takes the words after it, connective included -- and ``"Jane née and Jones Smith"`` reads maiden ``and Jones Smith`` where it read middle ``née and Jones``, family ``Smith`` -- the one name word the marker leaves goes to ``family`` since #445 below, and read as given ``Jane`` until it did. It also retires the particle chain's marker stop from the #399 fix above, whose condition restated the marker rule's and disagreed with it one suffix later (``"Jane van der Berg née Jr Jones"``, #417): a marker the rule declines -- nothing after it but a suffix -- is an ordinary word and rides inside the chain, which is the reading ``"Jane van der Berg née"`` already had, and the parsed fields of that name do not change. Two limits. A bound given-name word still never joins onto a marker standing as a word of its own, so ``"Berg, abdul née PhD"`` keeps given ``abdul``; a declined marker the particle chain has taken travels with the chain, so ``"Abd van der Berg née Jr Jones"`` reads given ``Abd van der Berg née`` where it read given ``Abd van der Berg``, middle ``née Jr``. And the marker rule now sees the words as written rather than as joined, so a suffix-vocabulary word inside the maiden name stops it even with a connective beside it: ``"Jane née Jr y Jones"`` read maiden ``Jr y Jones`` and now reads family ``Jr y Jones`` with no maiden name, and ``"Jane Smith née Jones Jr y Smith"`` read maiden ``Jones Jr y Smith`` and now reads maiden ``Jones``, family ``Jr y Smith``. One consequence: ``"abd née Jones Jr Smith Berg"`` reads given ``abd Jr`` -- as ``"abd Jr Smith Berg"`` already does -- where the #411 fix above read given ``abd``. Four of the 1025 differential corpus names move. The two connective-join names move at the 2.0.0 and 2.1.0 baselines alike; at 1.4.0 both were already classified as maiden-marker changes, 1.4 having no maiden field. ``"Juan y Garcia née Jones"`` and ``"Jane née Jr y Jones"``, the examples this fix adds to the rules doc and so to the rules corpus, move at all three and at 2.0.0/2.1.0 respectively. The eight corpus names that lose a family only under an APPENDED clause are not in the corpora in that form, so the gate cannot see them; the case table pins that half (closes #412, closes #417, closes #418) + - Change how far a leading never-given particle takes the surname when a family-first ``name_order`` is declared. ``Policy(name_order=FAMILY_FIRST)`` read ``"de Mesnil Juan"`` as family ``de Mesnil Juan`` -- the whole name -- and now reads family ``de Mesnil``, given ``Juan``. The default order is unchanged, deliberately: with no order declared nothing marks where the surname ends, and a particle followed by several words really can be all surname (``von Bergen Wessels``); a caller who means family ``de la Vega`` plus given ``Juan`` there writes the comma. The run takes one name WORD rather than one token, so the stop cannot land inside a conjunction-joined run or a bound given-name pair: ``"de la Vega y Santos Juan"`` reads family ``de la Vega y Santos``, ``"ibn Awf abdul Rahman"`` given ``abdul Rahman``. Where two or more words are left over, the two family-first orders differ from each other for the first time: ``"de la Cruz Juan Carlos"`` reads given ``Juan``, middle ``Carlos`` under ``FAMILY_FIRST`` and the reverse under ``FAMILY_FIRST_GIVEN_LAST``. An ambiguous leading particle and a family comma are both untouched. See the ``P1`` entry of ``docs/design/decisions.md`` (closes #395) - - Fix a title-plus-surname name losing its family name whenever anything stood beside it. ``"Dr. Smith"`` reads family ``Smith``, but ``"Dr. Smith née Jones"`` read given ``Smith`` with no family at all, and so did ``"Dr. Smith PhD"`` and ``"Dr. "Smitty" Smith"``. A title followed by a single name word names the family, and that rule declined whenever the name also carried a suffix, a nickname or a maiden name -- it counted them as further name words, which none of them is: each stands beside the name rather than in it, and none makes the name any longer. The rule now counts name words alone. ``"Dr. Smith née Jones"`` reads family ``Smith``, maiden ``Jones``; ``"Dr. Smith PhD"`` family ``Smith``, suffix ``PhD``; ``"Dr. "Smitty" Smith"`` family ``Smith``, nickname ``Smitty``; and the particle spelling this was found through, ``"Freiherr von Richthofen geb. Albrecht"``, reads family ``von Richthofen``, maiden ``Albrecht`` -- 2.2's own particle-chain stop had routed that canonical shape into the rule for the first time (#399). A given-name title is unchanged and still names no family: ``"Sir John née Jones"`` keeps given ``John`` with an empty family, exactly as ``"Sir John"`` does. One name moves where the nickname LEADS, rather than standing beside a name that already reads: ``"'Smitty' Dr. Jones"``. The lone-word nickname rule declines there because the title counts against its piece count, which leaves a title and one name word for this fix to read as the family -- given ``Jones`` through 2.1, family ``Jones`` now. Five names of the differential corpora change reading, the same five at every baseline. Four of them arrive as new diffs and are classified with this fix: ``"Dr. Smith née Jones"``, ``"Senator "Rick" Edmonds"``, ``"Xyz. (Bud) Smith"`` and ``"'Smitty' Dr. Jones"``. The fifth, ``"Freiherr von Richthofen V"``, was already classified under 2.2's particle-chain fix and stays there -- it is the suffix flavor in corpus form, and reads family ``von Richthofen`` with suffix ``V`` where it read given ``von Richthofen`` before. v1 read the two nicknames as 2.1 did; it had no maiden support at all, reading ``"Dr. Smith née Jones"`` as first ``Smith``, middle ``née``, last ``Jones``. The v1 test suite shipped the correct reading for the nickname shape as a known-failing test, which now passes (closes #410) + - Change the ``detail`` text of a ``PARTICLE_OR_GIVEN`` ambiguity to name the role the leading particle was actually given. It said "read as a given name" under every ``name_order``, which is false under ``Policy(name_order=FAMILY_FIRST)`` -- there ``"Van Johnson"`` reads family ``Van``, given ``Johnson``, and the report described the reading not taken. It now ends "read as a family name" in that case. The ``kind`` is unchanged and stays ``PARTICLE_OR_GIVEN``; only the human-readable text moved, and default-order output is identical (#355) - - Fix a name that is a surname and a maiden clause reporting no family at all. ``"Smith née Jones"`` read given ``Smith`` with an empty ``family``; it now reads family ``Smith``, maiden ``Jones``. A maiden marker announces a FORMER surname, and a former surname only means something beside a current one -- nobody writes a maiden clause where there is no surname for it to set apart -- so where the marker has taken its name and a single name word is left standing, that word is the surname the bearer uses now. With one name word and nothing else to go on the library has to pick a field and picks ``given``, which is a convention rather than a determination and is now written down as such; a maiden marker joins the title and the nickname as one of the things that decides such a name instead. Every spelling of the shape moves, wherever the clause stands and however it is written -- a marker taking the words after it, a marker inside a bracket, or a bracket pair you declared to mean maiden, which carries no marker at all: under ``Policy(maiden_delimiters=frozenset({("(", ")")}))``, ``"Smith (Jones)"`` reads family ``Smith``, maiden ``Jones``. ``"Smith (née Jones)"`` reads family ``Smith``, maiden ``Jones``, which RESTORES 1.4.0 -- v1 read family ``Smith`` with the clause as a nickname, and only the clause changes hands now. ``"Jane née Jones Smith"``, where the marker stands INSIDE the name and takes the rest of it, reads family ``Jane``, maiden ``Jones Smith``, which no earlier version read (1.4.0 gave first ``Jane``, middle ``née Jones``, last ``Smith``): the rule counts what the clause LEAVES rather than where the marker stood. An annotation beside the name is no part of it, so ``"Smith née Jones PhD"`` reads family ``Smith``, suffix ``PhD``, and ``"'Smitty' Jones Jr. née Smith"`` reads family ``Jones``. Two shapes deliberately do NOT move, and both witnesses are names the differential corpora already carried rather than cases constructed for the release: a word the vocabulary claims as a given name keeps that reading, so ``"abd née Jones"`` still reads given ``abd``, and so does a word written as an initial, so ``"J. née Jones Smith V"`` still reads given ``J.`` -- this rule changes what POSITION decided and does not reach what a word already is. A title still decides the name behind it: ``"Dr. Smith née Jones"`` reads family ``Smith`` as it does since the title fix above, and the given-name title in ``"Sir John née Jones"`` still keeps given ``John`` with no family. A name with two name words beside the clause is untouched -- ``"Jane Smith née Jones"`` reads given ``Jane``, family ``Smith``, maiden ``Jones`` as before. If you have code that reads the lone name word beside a maiden clause out of ``given``, this is the release where it moves to ``family``. Nine of the 1,090 differential corpus names change reading: eight arrive as new diffs at every baseline and are classified with this fix, and the ninth is ``"Smith (née Jones)"``, which every released version already read family ``Smith`` -- so its diff SHRANK, at all three baselines, to the clause changing hands (closes #445) + - Fix a maiden name being lost when a particle stood in front of the marker. ``"Ursula Leyen geb. Albrecht"`` reported maiden ``Albrecht`` correctly, but ``"Ursula von der Leyen geb. Albrecht"`` -- the same words one particle chain apart -- gave family ``von der Leyen geb. Albrecht`` and no maiden name at all, as did ``"Jane van der Berg née Jones"``. A suffix already stopped the particle chain; a marker now does too, so those read family ``von der Leyen`` maiden ``Albrecht`` and family ``van der Berg`` maiden ``Jones``. A single leading particle always worked (``"von Müller geb. Schmidt"``); a leading run of two did not. A marker needs a word after it to hand the maiden name to, so a trailing one bounds nothing and ``"Jane van der Berg née"`` keeps family ``van der Berg née`` -- which is what keeps ``"Jane van der Nee"`` intact, ``Nee`` being an attested surname as well as a marker spelling. Under a family-first order ``"de la Cruz née Vega"`` now reads family ``de la Cruz``, maiden ``Vega``. Two limits remain, both recorded in ``rules.md#M2``: a conjunction join and a bound given-name join each still absorb a marker first (closes #399) + + - 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"`` becomes last ``Mc Donald Smith``. The ``PARTICLE_OR_GIVEN`` ambiguity goes with it for ``mc`` and ``ste``, which only changed halves -- ``"Jack Mc Donald"`` 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"`` is now last ``das Neves``. Never-given membership goes from 28 words to 33 (closes #360) + + - 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 reads given ``abdul``, family ``Berg``; it now reads given ``abdul``, family ``Berg``, maiden ``Jones``. The join also declines when the piece it would absorb is a marker, so ``"van der Berg, abdul née Jones"`` reads given ``abdul``, family ``van der Berg``, maiden ``Jones`` where it read given ``abdul née``. A name with a word to spare is untouched (``"abd Allah Smith née Jones"`` still joins). Where the bound word is ALSO suffix vocabulary, a declining join after a family comma leaves the post-nominal reading: ``"Berg, abd née Jones"`` reads family ``Berg``, suffix ``abd``, maiden ``Jones``, as ``"Berg, abd"`` alone always has (closes #411) + + - Fix a maiden clause changing how the rest of the name is read, and a connective join keeping the marker in the surname. A maiden marker and the name it introduces are not part of the name they follow, but the grouping rules that count a name's words counted them anyway: ``"juan y garcia"`` reads given ``juan``, middle ``y``, family ``garcia``, while ``"juan y garcia nee jones"`` read given ``juan y garcia`` with NO family name at all -- eight more corpus names lose the family the same way under an appended clause. A name of two or more name words now reads as it reads without its maiden clause, plus the maiden name. The connective join used to merge the marker into a multi-word piece, so ``"Jane van der Berg née y Jones"`` kept family ``van der Berg née y Jones`` and now reads family ``van der Berg``, maiden ``y Jones``. Two limits: a bound given-name word still never joins onto a marker standing as a word of its own (``"Berg, abdul née PhD"``), and a suffix-vocabulary word inside the maiden name stops the marker, so ``"Jane née Jr y Jones"`` now reads family ``Jr y Jones`` with no maiden name (closes #412, closes #417, closes #418) + + - Fix a title-plus-surname name losing its family name whenever anything stood beside it. ``"Dr. Smith"`` reads family ``Smith``, but ``"Dr. Smith née Jones"`` read given ``Smith`` with no family at all, and so did ``"Dr. Smith PhD"`` and ``"Dr. "Smitty" Smith"``. A suffix, a nickname and a maiden name each stand beside the name rather than in it, and the rule now counts name words alone: those read family ``Smith`` with maiden ``Jones``, suffix ``PhD`` and nickname ``Smitty`` respectively, and ``"Freiherr von Richthofen geb. Albrecht"`` reads family ``von Richthofen``, maiden ``Albrecht``. A given-name title still names no family: ``"Sir John née Jones"`` keeps given ``John``, exactly as ``"Sir John"`` does. One name moves where the nickname LEADS: ``"'Smitty' Dr. Jones"`` reads family ``Jones`` where it read given (closes #410) + + - Fix a name that is a surname and a maiden clause reporting no family at all. ``"Smith née Jones"`` read given ``Smith`` with an empty ``family``; it now reads family ``Smith``, maiden ``Jones``. A maiden marker announces a FORMER surname, and a former surname only means something beside a current one, so the single name word left standing is the surname the bearer uses now. Every spelling of the shape moves, including a bracket pair you declared to mean maiden: under ``Policy(maiden_delimiters=frozenset({("(", ")")}))``, ``"Smith (Jones)"`` reads family ``Smith``, maiden ``Jones``. ``"Smith (née Jones)"`` RESTORES 1.4.0, which read family ``Smith`` with the clause as a nickname. An annotation beside the name is no part of it, so ``"Smith née Jones PhD"`` reads family ``Smith``, suffix ``PhD``. Two shapes deliberately do NOT move: a word the vocabulary claims as a given name (``"abd née Jones"``) and a word written as an initial (``"J. née Jones Smith V"``) -- this rule changes what POSITION decided and does not reach what a word already is. **If you have code that reads the lone name word beside a maiden clause out of ``given``, this is the release where it moves to ``family``** (closes #445) **Deprecations** @@ -123,7 +118,7 @@ Release Log * - ``suffixes.SUFFIX_NOT_ACRONYMS`` - ``suffixes.SUFFIX_WORDS`` - Every row above still resolves and is removed in 3.0. The two module rows are import paths: importing them still works and says nothing, since both modules are now empty shims. Reading a *constant* -- by attribute access, by ``from ... import``, or by ``from ... import *`` -- emits a ``DeprecationWarning`` naming the module and constant to move to, once per line that reads it rather than once per process, so every place you have to edit is reported rather than only whichever one ran first. ``python -W error::DeprecationWarning -c "import yourapp"`` surfaces them; Python hides ``DeprecationWarning`` outside ``__main__``. Two of the four kept their module, so only the constant moved there. ``SUFFIX_NOT_ACRONYMS`` was also inaccurate as well as dated — ``esq`` is in ``SUFFIX_ACRONYMS`` too. The ``CONSTANTS`` attribute names (``prefixes``, ``non_first_name_prefixes``, ``bound_first_names``, ``first_name_titles``, ``suffix_not_acronyms``) are v1 facade surface and are unchanged. See :doc:`migrate` (#293) + Every row above still resolves and is removed in 3.0. The two module rows are import paths: importing them still works and says nothing, since both modules are now empty shims. Reading a *constant* -- by attribute access, by ``from ... import``, or by ``from ... import *`` -- emits a ``DeprecationWarning`` naming the module and constant to move to, once per line that reads it rather than once per process, so every place you have to edit is reported rather than only whichever one ran first. ``python -W error::DeprecationWarning -c "import yourapp"`` surfaces them; Python hides ``DeprecationWarning`` outside ``__main__``. The ``CONSTANTS`` attribute names (``prefixes``, ``non_first_name_prefixes``, ``bound_first_names``, ``first_name_titles``, ``suffix_not_acronyms``) are v1 facade surface and are unchanged. See :doc:`migrate` (#293) * 2.1.0 - August 7, 2026 From ea04f2ce047407cc49716fb2c0c688b1f63b51d2 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Mon, 31 Aug 2026 15:21:35 -0700 Subject: [PATCH 02/10] docs(customize,rules): a multi-word credential is reachable, the entry is not customize.rst and rules.md's suffix Background both said, truly, that a multi-word vocabulary entry can never match -- and neither said the next thing, whose absence is what does the damage: adjacent suffix tokens are reassembled after matching (_vocab.is_wholly_suffix), so a multi-word credential parses as its component words. parse("John Smith, MD PhD") has read suffix 'MD PhD' since 1.4.0. The inference from "a multi-word ENTRY is inert" to "a multi-word CREDENTIAL is unparseable" survived #291, an approved design spec and a full implementation plan specifying a new vocabulary set, matching unit, predicate and Lexicon field. None of them ran the parse. No shipped doc ever stated the false claim -- a true limitation sitting next to an unstated capability is the shape that produced it, so stating the capability is the fix. The recipe is a teaching example rather than a pinning one, so it earns its doctest under the recipe carve-out. Both examples verified non-vacuous: on the default vocabulary "John Smith, LEED AP" gives suffix '' and family 'John Smith'. Closes #433 Co-Authored-By: Claude Opus 5 --- docs/customize.rst | 20 ++++++++++++++++++++ docs/design/rules.md | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/customize.rst b/docs/customize.rst index 6f8a3ddc..80555651 100644 --- a/docs/customize.rst +++ b/docs/customize.rst @@ -66,6 +66,26 @@ it matches and the word matches everywhere else. No warning is raised for a multi-word entry in either of these two fields, since there it is not a mistake. +The limit is on *storage*, not on the shape a name can have. Adjacent +suffix words are reassembled after they match, so a multi-word +credential is reachable as its component words even though the phrase +itself cannot be stored: + +.. doctest:: + + >>> from nameparser import parse + >>> parse("John Smith, MD PhD").suffix + 'MD PhD' + +That has held since 1.4.0. A credential whose words are not in the +default vocabulary is reached by adding those words, not the phrase: + +.. doctest:: + + >>> lex = Lexicon.default().add(suffix_acronyms={"leed", "ap"}) + >>> Parser(lexicon=lex).parse("John Smith, LEED AP").suffix + 'LEED AP' + Removing works the same way, and drops the word from recognition: .. doctest:: diff --git a/docs/design/rules.md b/docs/design/rules.md index 7dfc812d..f1d8e56f 100644 --- a/docs/design/rules.md +++ b/docs/design/rules.md @@ -500,7 +500,7 @@ P6. Rationale: a particle ending the name has nothing to link ## Suffixes: generational & credentials (S) -Background: what follows a name is one of two different things — generational suffixes (Jr., III), which attach to the name itself, and credentials (PhD, MD, MBA), which are earned attachments. The suffix sets match one written word at a time: a multi-word entry there can never match anything and is warned about at configuration. Only two sets are exempt from that rule -- given_name_titles and, since #434, maiden_markers -- and neither is a suffix set, so within the suffix vocabulary the one-word rule is absolute. The eight multi-word entries that shipped dead for years span the suffix sets and the titles alike and are the Excluded story in decisions.md. CLDR personNames keeps them as separate fields (`generation`, `credentials`) and formats them differently; this library currently reports both in one `suffix` field, a merge #326 examines. The vocabulary is largely split already: a generational word list and a credential acronym list, plus a short list of acronyms that are also ordinary names (MA, BA) and so are AMBIGUOUS as bare words. +Background: what follows a name is one of two different things — generational suffixes (Jr., III), which attach to the name itself, and credentials (PhD, MD, MBA), which are earned attachments. The suffix sets match one written word at a time: a multi-word entry there can never match anything and is warned about at configuration. Only two sets are exempt from that rule -- given_name_titles and, since #434, maiden_markers -- and neither is a suffix set, so within the suffix vocabulary the one-word rule is absolute. That limit is on STORAGE, not on the shape a name may have: adjacent suffix tokens are reassembled after matching (`_vocab.is_wholly_suffix`), so a multi-word credential is reachable as its component words -- `John Smith, MD PhD` has read suffix `MD PhD` since 1.4.0 -- and a caller reaches an unshipped one by adding the words it is made of rather than the phrase (#433). The eight multi-word entries that shipped dead for years span the suffix sets and the titles alike and are the Excluded story in decisions.md. CLDR personNames keeps them as separate fields (`generation`, `credentials`) and formats them differently; this library currently reports both in one `suffix` field, a merge #326 examines. The vocabulary is largely split already: a generational word list and a credential acronym list, plus a short list of acronyms that are also ordinary names (MA, BA) and so are AMBIGUOUS as bare words. S1. Rationale: brackets set off more than nicknames — credentials are routinely written parenthesized after a name, and a From 8ff01e31d777570360f56f16eb960729f9998707 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Mon, 31 Aug 2026 15:26:36 -0700 Subject: [PATCH 03/10] chore(config,tests): the #293 rename housekeeping deferred out of PR #354 Six items from PR #354's five-agent review, none touching behaviour. The two migration DESTINATIONS never got the __all__ their 1.x shims gained in that PR, so `from nameparser.config.particles import *` bound `assert_normalized` and BOUND_GIVEN_NAMES -- the latter owned by another module and imported only for a disjointness assert -- as though they were vocabulary. Both now declare one, in source order for autodoc, per the note in suffixes.py. alias_getattr's __dir__ read `sys.modules[module]`, so dir() on a module dropped from sys.modules raised KeyError, which is not among the things dir() may do to a caller. It degrades to the closure's aliases instead. In the test file: ALIASES becomes a NamedTuple (six `for m, n, _, _ in ALIASES` sites read correctly only if you remember which end is which; _LatinCopy in test_ledger_guards.py is the precedent); _RETIRED_NAMES is derived from it rather than hand-written a second time in the same file, verified identical to the five rows it replaces; test_star_import_binds_exactly_the_live_and_retired_names asserts VALUES as well as names; and the from-import attribution test runs over all five aliases rather than the one prefixes row -- the two that KEPT their module are a different shape, their table sitting at the bottom of a module mid-execution during its own import. Both fixes mutation-checked rather than merely covered. Routing prefixes.PREFIXES at NON_GIVEN_NAME_PARTICLES fails the new value assertion while the pre-existing name assertion passes it, the bound set being identical either way; restoring the sys.modules subscript fails the new dir() test on all four shims. Already fixed since the issue was filed, verified rather than assumed: customize.rst names REGEXES/CAPITALIZATION_EXCEPTIONS as the freeze exceptions and says TITLES was not renamed; AGENTS.md carries the measured esq claim, the 2.0 note on the delimiter sentinel, and the per-read-location warning semantics; and no gitignored-spec citation survives in the package (test_doc_citations enforces it). AGENTS.md's join line sits under the v1 parser walkthrough, where join_on_conjunctions and lastname_pieces are v1's own identifiers. Closes #356 Co-Authored-By: Claude Opus 5 --- docs/release_log.rst | 1 + nameparser/config/_deprecated.py | 10 +- nameparser/config/bound_given_names.py | 6 ++ nameparser/config/particles.py | 11 ++ tests/v2/test_config_aliases.py | 141 +++++++++++++++++++------ 5 files changed, 136 insertions(+), 33 deletions(-) diff --git a/docs/release_log.rst b/docs/release_log.rst index 69618aa6..ed647d2d 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -24,6 +24,7 @@ Release Log - Add ``docs/design/`` contributor documentation: ``rules.md`` (the parser's normative rules, with executable examples), ``decisions.md`` (the decision record) and ``mechanisms.md`` (the solution-pattern catalog). New tests execute every documented example and verify every code citation - Change every vocabulary set in ``nameparser.config`` to a ``frozenset``. Editing one in place -- ``TITLES.add("dean")``, the old way of changing a global default -- now raises ``AttributeError`` at the line that writes it. It was never a reliable way to change a default: whether an edit reached a given parse depended on which config objects had already been built, so one program could hold two disagreeing defaults with nothing to say so. To change the defaults for ``HumanName``, build a private ``Constants`` and pass it (``c = Constants(); c.titles.add("dean"); HumanName(name, constants=c)``); for the 2.0 API, build a lexicon (``Parser(lexicon=Lexicon.default().add(titles={"dean"}))``). Mutating the shared ``CONSTANTS`` still works, but warns and goes away in 3.0. ``CAPITALIZATION_EXCEPTIONS`` is a mapping, not a set, and is unchanged. See :doc:`migrate` and :doc:`customize` (#293) + - Change what a star import of the two 2.2 vocabulary modules binds. ``nameparser.config.particles`` and ``nameparser.config.bound_given_names`` now declare ``__all__``, which their 1.x shims already did, so ``from ... import *`` binds their vocabulary alone -- it also bound the ``assert_normalized`` invariant helper, and from ``particles`` the ``BOUND_GIVEN_NAMES`` it imports only for a disjointness check. Importing a constant by name is unaffected (#356) **Behavior Changes** diff --git a/nameparser/config/_deprecated.py b/nameparser/config/_deprecated.py index 60b9f90e..c688705e 100644 --- a/nameparser/config/_deprecated.py +++ b/nameparser/config/_deprecated.py @@ -118,7 +118,15 @@ def __dir__() -> list[str]: # of tab completion and every getattr-free member scan -- # autodoc's included. Pinned by test_config_aliases # ::test_dir_lists_the_live_names_as_well_as_the_retired_ones. - return sorted(set(vars(sys.modules[module])) | set(aliases)) + # + # .get, not [...]: a module dropped from sys.modules -- a test + # that reloads the package, a plugin teardown -- would otherwise + # make dir() raise KeyError, which is not among the things dir() + # may do to a caller. The aliases are held in the closure and + # are still nameable, so they are what is left to list (#356). + live = sys.modules.get(module) + names = set(vars(live)) if live is not None else set() + return sorted(names | set(aliases)) # The table itself, reachable without tripping a warning. __all__ is # hand-written per module (it must stay in SOURCE order for autodoc, diff --git a/nameparser/config/bound_given_names.py b/nameparser/config/bound_given_names.py index fb11becd..56d263df 100644 --- a/nameparser/config/bound_given_names.py +++ b/nameparser/config/bound_given_names.py @@ -52,3 +52,9 @@ assert_normalized("BOUND_GIVEN_NAMES", BOUND_GIVEN_NAMES) + +# Star imports read __all__ and never the module __getattr__ -- see the +# note in prefixes.py. Without it `assert_normalized`, imported only for +# the invariant above, is bound by a star import as though it were +# vocabulary (#356). +__all__ = ["BOUND_GIVEN_NAMES"] diff --git a/nameparser/config/particles.py b/nameparser/config/particles.py index 79658fda..62696765 100644 --- a/nameparser/config/particles.py +++ b/nameparser/config/particles.py @@ -262,3 +262,14 @@ assert not (NON_GIVEN_NAME_PARTICLES & BOUND_GIVEN_NAMES), \ "NON_GIVEN_NAME_PARTICLES must stay disjoint from BOUND_GIVEN_NAMES" assert_normalized("PARTICLES", PARTICLES) + +# Star imports read __all__ and never the module __getattr__ -- see the +# note in prefixes.py. This module has no retired name of its own to +# serve; what __all__ buys here is the other half of that note's point, +# which the shim modules got in #354 and the destinations did not: +# without it a star import binds every module-level name, so +# `assert_normalized` and BOUND_GIVEN_NAMES -- imported only for the +# invariants below, and owned by another module -- landed in the +# caller's namespace as though they were vocabulary (#356). +# Source order, not alphabetical -- see the note in suffixes.py. +__all__ = ["NON_GIVEN_NAME_PARTICLES", "PARTICLES"] diff --git a/tests/v2/test_config_aliases.py b/tests/v2/test_config_aliases.py index 4093aacb..44eb641f 100644 --- a/tests/v2/test_config_aliases.py +++ b/tests/v2/test_config_aliases.py @@ -10,31 +10,56 @@ import importlib import inspect import pathlib +import sys import warnings +from typing import NamedTuple import pytest import nameparser -#: (old module, old name, new module, new name), one row per alias. + +class _Alias(NamedTuple): + """One retired 1.x vocabulary name and the 2.2 name serving it. + + Four same-typed strings whose order carries all the meaning, which + is what makes a bare tuple the wrong shape here: ``for m, n, _, _ in + ALIASES`` appeared six times below and reads correctly only if you + remember which end is which. Named after ``_LatinCopy`` in + ``test_ledger_guards.py`` (#356). + """ + old_module: str + old_name: str + new_module: str + new_name: str + + +def _id(alias: _Alias) -> str: + return f"{alias.old_module.rsplit('.', 1)[-1]}.{alias.old_name}" + + +#: One row per alias. ALIASES = [ - ("nameparser.config.prefixes", "PREFIXES", - "nameparser.config.particles", "PARTICLES"), - ("nameparser.config.prefixes", "NON_FIRST_NAME_PREFIXES", - "nameparser.config.particles", "NON_GIVEN_NAME_PARTICLES"), - ("nameparser.config.bound_first_names", "BOUND_FIRST_NAMES", - "nameparser.config.bound_given_names", "BOUND_GIVEN_NAMES"), - ("nameparser.config.titles", "FIRST_NAME_TITLES", - "nameparser.config.titles", "GIVEN_NAME_TITLES"), - ("nameparser.config.suffixes", "SUFFIX_NOT_ACRONYMS", - "nameparser.config.suffixes", "SUFFIX_WORDS"), + _Alias("nameparser.config.prefixes", "PREFIXES", + "nameparser.config.particles", "PARTICLES"), + _Alias("nameparser.config.prefixes", "NON_FIRST_NAME_PREFIXES", + "nameparser.config.particles", "NON_GIVEN_NAME_PARTICLES"), + _Alias("nameparser.config.bound_first_names", "BOUND_FIRST_NAMES", + "nameparser.config.bound_given_names", "BOUND_GIVEN_NAMES"), + _Alias("nameparser.config.titles", "FIRST_NAME_TITLES", + "nameparser.config.titles", "GIVEN_NAME_TITLES"), + _Alias("nameparser.config.suffixes", "SUFFIX_NOT_ACRONYMS", + "nameparser.config.suffixes", "SUFFIX_WORDS"), ] +#: Old module paths, one per shim, for the per-module parametrizations. +OLD_MODULES = sorted({a.old_module for a in ALIASES}) + @pytest.mark.parametrize( ("old_module", "old_name", "new_module", "new_name"), ALIASES, - ids=[f"{m.rsplit('.', 1)[-1]}.{n}" for m, n, _, _ in ALIASES], + ids=[_id(a) for a in ALIASES], ) def test_old_name_warns_and_resolves_to_the_new_constant( old_module: str, old_name: str, new_module: str, new_name: str, @@ -65,12 +90,26 @@ def test_warning_points_at_the_line_that_read_the_name() -> None: assert (record[0].filename, record[0].lineno) == (__file__, expected_lineno) -def test_from_import_is_attributed_to_the_importing_module() -> None: +@pytest.mark.parametrize( + ("old_module", "old_name"), + [(a.old_module, a.old_name) for a in ALIASES], + ids=[_id(a) for a in ALIASES], +) +def test_from_import_is_attributed_to_the_importing_module( + old_module: str, old_name: str, +) -> None: """The form the ``stacklevel`` comment singles out, and the one most callers use. ``from x import Y`` resolves the alias while the importing module's frame is on top, so the report names the file - holding the import -- the line that has to be edited.""" - code = compile("from nameparser.config.prefixes import PREFIXES\n", + holding the import -- the line that has to be edited. + + Every alias, not the one ``prefixes`` row this covered until #356. + The two that KEPT their module are a structurally different shape: + their table sits at the bottom of a module that is mid-execution + during its own import, so a regression could reach those two and not + the moved ones. + """ + code = compile(f"from {old_module} import {old_name}\n", "caller_module.py", "exec") with pytest.warns(DeprecationWarning) as record: exec(code, {"__name__": "caller_module"}) @@ -79,8 +118,8 @@ def test_from_import_is_attributed_to_the_importing_module() -> None: @pytest.mark.parametrize( ("old_module", "old_name"), - [(m, n) for m, n, _, _ in ALIASES], - ids=[f"{m.rsplit('.', 1)[-1]}.{n}" for m, n, _, _ in ALIASES], + [(a.old_module, a.old_name) for a in ALIASES], + ids=[_id(a) for a in ALIASES], ) def test_old_name_warns_once_per_read_location( old_module: str, old_name: str, @@ -120,7 +159,7 @@ def test_old_name_warns_once_per_read_location( @pytest.mark.parametrize( - "old_module", sorted({m for m, _, _, _ in ALIASES})) + "old_module", OLD_MODULES) def test_unknown_attribute_still_raises(old_module: str) -> None: module = importlib.import_module(old_module) with pytest.raises(AttributeError, match="NOT_A_CONSTANT"): @@ -129,13 +168,32 @@ def test_unknown_attribute_still_raises(old_module: str) -> None: @pytest.mark.parametrize( ("old_module", "old_name"), - [(m, n) for m, n, _, _ in ALIASES], - ids=[f"{m.rsplit('.', 1)[-1]}.{n}" for m, n, _, _ in ALIASES], + [(a.old_module, a.old_name) for a in ALIASES], + ids=[_id(a) for a in ALIASES], ) def test_dir_advertises_the_old_names(old_module: str, old_name: str) -> None: assert old_name in dir(importlib.import_module(old_module)) +@pytest.mark.parametrize("old_module", OLD_MODULES) +def test_dir_survives_the_module_being_dropped_from_sys_modules( + old_module: str, monkeypatch: pytest.MonkeyPatch, +) -> None: + """``dir()`` may raise ``AttributeError``; it may not raise ``KeyError``. + + The override reads the module back out of ``sys.modules`` to reach + the live names, and a subscript there made ``dir(module)`` raise + ``KeyError`` for anyone holding a reference to a module that had + been dropped -- a package reload, a plugin teardown. The aliases + live in the closure and are still nameable, so they are what a + degraded listing can honestly offer (#356). + """ + module = importlib.import_module(old_module) + retired = {a.old_name for a in ALIASES if a.old_module == old_module} + monkeypatch.delitem(sys.modules, old_module) + assert set(dir(module)) == retired + + def test_dir_lists_the_live_names_as_well_as_the_retired_ones() -> None: """The other half of what these four ``__dir__`` overrides owe. @@ -154,7 +212,7 @@ def test_dir_lists_the_live_names_as_well_as_the_retired_ones() -> None: """ live_seen = [] dropped = {} - for old_module in sorted({m for m, _, _, _ in ALIASES}): + for old_module in OLD_MODULES: module = importlib.import_module(old_module) missing = set(vars(module)) - set(dir(module)) if missing: @@ -174,7 +232,7 @@ def test_dir_lists_the_live_names_as_well_as_the_retired_ones() -> None: @pytest.mark.parametrize( - "old_module", sorted({m for m, _, _, _ in ALIASES})) + "old_module", OLD_MODULES) def test_every_alias_table_row_reaches_star_import(old_module: str) -> None: """The direction the star-import test cannot see. @@ -200,14 +258,14 @@ def test_every_alias_table_row_reaches_star_import(old_module: str) -> None: f"{old_module} serves {sorted(set(table) - exported)} through " f"__getattr__ but omits it from __all__, so `from {old_module} " f"import *` drops the name silently") - assert set(table) == {n for m, n, _, _ in ALIASES if m == old_module}, ( + assert set(table) == {a.old_name for a in ALIASES if a.old_module == old_module}, ( f"{old_module}'s alias table and this file's ALIASES disagree; " f"the literal table here is what proves the bridge points where " f"the migration guide says, so it has to cover every row") @pytest.mark.parametrize( - "old_module", sorted({m for m, _, _, _ in ALIASES})) + "old_module", OLD_MODULES) def test_star_import_binds_exactly_the_live_and_retired_names( old_module: str, ) -> None: @@ -226,7 +284,7 @@ def test_star_import_binds_exactly_the_live_and_retired_names( kept in step by the same person who forgot ``__all__``. """ module = importlib.import_module(old_module) - retired = {n for m, n, _, _ in ALIASES if m == old_module} + retired = {a.old_name for a in ALIASES if a.old_module == old_module} # A retired name is served by __getattr__ and never written into the # module, so vars() holds the live constants -- plus whatever else # the file imported. The type test is what separates the two: @@ -244,6 +302,21 @@ def test_star_import_binds_exactly_the_live_and_retired_names( bound = {n for n in namespace if not n.startswith("__")} assert bound == live | retired + # NAMES alone would pass with an __all__ entry routed to the wrong + # constant -- the set is the same either way. Each retired name is + # checked against its destination and each live one against the + # module, so a mis-wired row fails here rather than in whatever + # parses differently three releases later (#356). + for alias in (a for a in ALIASES if a.old_module == old_module): + destination = getattr( + importlib.import_module(alias.new_module), alias.new_name) + assert namespace[alias.old_name] is destination, ( + f"`from {old_module} import *` bound {alias.old_name} to " + f"something other than {alias.new_module}.{alias.new_name}") + for name in live: + assert namespace[name] is vars(module)[name], ( + f"`from {old_module} import *` bound {name} to something " + f"other than the module's own constant") # the helper the bridge is built from is not vocabulary; before # __all__ it was the only thing a star import bound here assert "alias_getattr" not in bound @@ -279,14 +352,18 @@ def test_star_import_binds_exactly_the_live_and_retired_names( #: ``TITLE_PREFIXES`` reports as ``PREFIXES``. Renaming is the wrong #: advice there, so the failure message offers this allow-list as the #: other remedy. +#: DERIVED from ``ALIASES``, not listed again: the allow-listed file is +#: always the module that serves the name -- for the three that moved +#: because that is where the alias table sits, and for the two that kept +#: their module because the table sits at the bottom of the file whose +#: constant it renames. A second hand-written copy of the same five +#: names in the same file is drift surface: a sixth alias added to one +#: list and not the other would silently exempt that name from the scan +#: below rather than failing (#356). _RETIRED_NAMES = { - "PREFIXES": ("config/prefixes.py",), - "NON_FIRST_NAME_PREFIXES": ("config/prefixes.py",), - "BOUND_FIRST_NAMES": ("config/bound_first_names.py",), - # these two kept their module; the exemption is for the alias table - # at the bottom of the file, which names them as strings - "FIRST_NAME_TITLES": ("config/titles.py",), - "SUFFIX_NOT_ACRONYMS": ("config/suffixes.py",), + a.old_name: ( + a.old_module.removeprefix("nameparser.").replace(".", "/") + ".py",) + for a in ALIASES } From 3473fdcc49858b8fa2f7eeb6fc9f05859bce1c6f Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Mon, 31 Aug 2026 15:31:48 -0700 Subject: [PATCH 04/10] docs(concepts,usage,group): three stale behavior claims, measured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release-checklist sweep (AGENTS.md step 0), run against what 2.2 itself changed. Two of the three were broken by #296 in this same cycle, which is the shape the sweep exists for: prose describes behavior in words, so grepping the changed SYMBOL finds none of it. concepts.rst said `Dr.` is "a title before a name and a suffix after it", the worked example for why the fields are really pre- and post-nominal. #296 took `dr` out of the suffix vocabulary, so "John Smith Dr." gives family 'Dr.' (measured). Replaced with `Lt.`, one of the twelve deliberate duals that audit kept: "Lt. Smith" is title 'Lt.' and "John Smith LT" is suffix 'LT'. usage.rst's CJK credentials section said the post-comma credential lands in `title` for `PhD`. #296 took `phd` out of TITLES: measured, "田中さん, PhD" gives suffix 'さん, PhD'. Three landings collapse to two, so the sentence says so -- `given` for `V.`, `PhD` and `Ph. D.` in `suffix`. _group.py's stage header declared Policy.extra_suffix_delimiters "-- no other Policy field" while reading policy.lenient_comma_suffixes at the segment_suffix_reading call. That read arrived with the #430/#432 review round (986e9d6) and the header was never updated. A false universal in a Reads: block is worse than an omission: it is the thing the next person checks against instead of the code. Verified accurate and left alone, so the sweep is on record as having covered them: modules.rst documents every live config module and correctly omits the two retired shims; every Policy field appears in customize.rst; the maiden and maiden_delimiters docs already carry the #335 behavior; migrate.rst's 2.2 rename section is current including the per-read-location warning semantics; concepts.rst's eight-token and Span(0,3) example, its STABLE_TAGS list, its nickname_delimiters, Ma-comma and "Joao da Silva do Amaral de Souza" claims, and usage.rst's "John Smith PhD MD" -> 'PhD, MD' all measure as written. No open Dependabot PRs on uv.lock. Co-Authored-By: Claude Opus 5 --- docs/concepts.rst | 7 ++++--- docs/usage.rst | 4 ++-- nameparser/_pipeline/_group.py | 8 +++++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/concepts.rst b/docs/concepts.rst index b39cd841..6d0ce6f6 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -66,9 +66,10 @@ else does. This is the whole parser in two sentences, and it explains its character. A word nameparser has never seen still gets a sensible role, because the positional layer does not need to recognize anything. The -same word can play different parts in different places — ``Dr.`` is a -title before a name and a suffix after it, which is why the field names -``title`` and ``suffix`` are really "pre-nominal" and "post-nominal". +same word can play different parts in different places — ``Lt.`` is a +title before a name and a post-nominal after it, which is why the field +names ``title`` and ``suffix`` are really "pre-nominal" and +"post-nominal". And nothing is statistical: there is no model and no training data, so the same input always parses the same way, and a parse that is wrong is wrong reproducibly, which is what makes it fixable by configuration. diff --git a/docs/usage.rst b/docs/usage.rst index c09ca979..fc766fe0 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -424,8 +424,8 @@ discard the only one there is. So ``田中さん, V.`` and ``田中さん, Ph. D.`` give さん up exactly as ``田中さん, PhD`` does. Where the credential itself lands is the comma's business rather than the peel's, and still differs by -spelling: ``title`` for ``PhD``, ``given`` for ``V.``, and ``Ph. D.`` -in ``suffix`` beside さん. +spelling: ``given`` for ``V.``, while ``PhD`` and ``Ph. D.`` join さん +in ``suffix``. Spacing, and where the name divides ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/nameparser/_pipeline/_group.py b/nameparser/_pipeline/_group.py index 02a49121..75249617 100644 --- a/nameparser/_pipeline/_group.py +++ b/nameparser/_pipeline/_group.py @@ -7,9 +7,11 @@ tokens are NEVER joined into strings: the anti-#100 invariant); maiden tail tokens get role=MAIDEN; marker tokens land in dropped. Reads: token tags (from classify), Lexicon.given_name_titles (the -P5 licence, #369), and Policy.extra_suffix_delimiters, whose -delimiter-core tokens tail segments drop (v1 suffix_delimiter parity) --- no other Policy field. The v1 "derived titles/prefixes" +P5 licence, #369), Policy.extra_suffix_delimiters, whose +delimiter-core tokens tail segments drop (v1 suffix_delimiter parity), +and Policy.lenient_comma_suffixes, which segment_suffix_reading takes +to pick the strict or lenient token test (#430/#432) -- no other Policy +field. The v1 "derived titles/prefixes" registration becomes piece_tags entries -- per-parse state that dissolves with the state (v1 kept per-parse sets for the same reason). From 69434a4e03ba2acfa0e32a66bfa359cc36b98264 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Mon, 31 Aug 2026 19:28:59 -0700 Subject: [PATCH 05/10] docs(release_log): the star-import change is not a breaking change Derek's call. Moved out of Breaking Changes to the end of Behavior Changes, and the bullet now says outright that no parse changes. What `__all__` removed from `from nameparser.config.particles import *` is `assert_normalized` and another module's BOUND_GIVEN_NAMES -- names nobody imports these modules for, bound only because the destinations never got the `__all__` their 1.x shims did. Backwards-incompatible in the letter, not in anything a caller wrote. The PR's breaking-change label came off with it; the two were one classification and had to move together. Co-Authored-By: Claude Opus 5 --- docs/release_log.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release_log.rst b/docs/release_log.rst index ed647d2d..22a29af9 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -24,7 +24,6 @@ Release Log - Add ``docs/design/`` contributor documentation: ``rules.md`` (the parser's normative rules, with executable examples), ``decisions.md`` (the decision record) and ``mechanisms.md`` (the solution-pattern catalog). New tests execute every documented example and verify every code citation - Change every vocabulary set in ``nameparser.config`` to a ``frozenset``. Editing one in place -- ``TITLES.add("dean")``, the old way of changing a global default -- now raises ``AttributeError`` at the line that writes it. It was never a reliable way to change a default: whether an edit reached a given parse depended on which config objects had already been built, so one program could hold two disagreeing defaults with nothing to say so. To change the defaults for ``HumanName``, build a private ``Constants`` and pass it (``c = Constants(); c.titles.add("dean"); HumanName(name, constants=c)``); for the 2.0 API, build a lexicon (``Parser(lexicon=Lexicon.default().add(titles={"dean"}))``). Mutating the shared ``CONSTANTS`` still works, but warns and goes away in 3.0. ``CAPITALIZATION_EXCEPTIONS`` is a mapping, not a set, and is unchanged. See :doc:`migrate` and :doc:`customize` (#293) - - Change what a star import of the two 2.2 vocabulary modules binds. ``nameparser.config.particles`` and ``nameparser.config.bound_given_names`` now declare ``__all__``, which their 1.x shims already did, so ``from ... import *`` binds their vocabulary alone -- it also bound the ``assert_normalized`` invariant helper, and from ``particles`` the ``BOUND_GIVEN_NAMES`` it imports only for a disjointness check. Importing a constant by name is unaffected (#356) **Behavior Changes** @@ -94,6 +93,8 @@ Release Log - Fix a name that is a surname and a maiden clause reporting no family at all. ``"Smith née Jones"`` read given ``Smith`` with an empty ``family``; it now reads family ``Smith``, maiden ``Jones``. A maiden marker announces a FORMER surname, and a former surname only means something beside a current one, so the single name word left standing is the surname the bearer uses now. Every spelling of the shape moves, including a bracket pair you declared to mean maiden: under ``Policy(maiden_delimiters=frozenset({("(", ")")}))``, ``"Smith (Jones)"`` reads family ``Smith``, maiden ``Jones``. ``"Smith (née Jones)"`` RESTORES 1.4.0, which read family ``Smith`` with the clause as a nickname. An annotation beside the name is no part of it, so ``"Smith née Jones PhD"`` reads family ``Smith``, suffix ``PhD``. Two shapes deliberately do NOT move: a word the vocabulary claims as a given name (``"abd née Jones"``) and a word written as an initial (``"J. née Jones Smith V"``) -- this rule changes what POSITION decided and does not reach what a word already is. **If you have code that reads the lone name word beside a maiden clause out of ``given``, this is the release where it moves to ``family``** (closes #445) + - Change what a star import of the two 2.2 vocabulary modules binds. ``nameparser.config.particles`` and ``nameparser.config.bound_given_names`` now declare ``__all__``, which their 1.x shims already did, so ``from ... import *`` binds their vocabulary alone -- it also bound the ``assert_normalized`` invariant helper, and from ``particles`` the ``BOUND_GIVEN_NAMES`` it imports only for a disjointness check. Importing a constant by name is unaffected, and no parse changes (#356) + **Deprecations** - Rename the four vocabularies whose 1.x names described the fields they feed in v1's words, so the data layer matches the ``Lexicon``: From 1c32a849afa2cf50f7bee6b2d297c4727ac36e16 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Mon, 31 Aug 2026 19:35:32 -0700 Subject: [PATCH 06/10] docs(release_log): a bold lead sentence on every substantial bullet Derek's ask: make the log scannable, so a reader can decide from the lead whether a bullet is worth reading. The existing first sentences were already written as identifiers, so they are what got bolded. Applied to bullets of 25 words or more, which scopes it by itself: 2.2.0, 2.1.0 and 2.0.0 come out near-fully bolded while the 1.x history is mostly untouched, its bullets being one-liners where the whole bullet IS the identifier and bolding it would defeat the purpose. 134 leads in all. The list-table rows fall below the threshold and are left alone. INLINE MARKUP DOES NOT NEST IN RST, and it fails silently -- measured through docutils rather than assumed: a ``literal``, a :role:`target`, an *emphasis* or a `link `_ inside ** ** renders its own punctuation verbatim, with NO warning, so the page would have shipped visible double backticks. Every lead is reduced to plain text before being wrapped; a role keeps its last dotted segment (:data:`~...BOUND_GIVEN_NAMES` -> BOUND_GIVEN_NAMES). The identifiers appear again in each bullet's body with their markup intact. Two bullets whose first sentence ran past the limit (#467, #360) were split by hand rather than left unbolded, preserving every fact. Also fixes two bold call-outs written earlier in this same branch that had literals inside them and carried the same silent defect. Verified on the RENDERED HTML, not the source: zero spans contain a stray backtick or asterisk across all 181 of them, and ``add(**entries)`` still renders correctly -- ** inside an inline literal is plain text to RST, which is why the source-level check reports it and the build does not. Co-Authored-By: Claude Opus 5 --- docs/release_log.rst | 272 +++++++++++++++++++++---------------------- 1 file changed, 136 insertions(+), 136 deletions(-) diff --git a/docs/release_log.rst b/docs/release_log.rst index 22a29af9..183e4be5 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -22,78 +22,78 @@ Release Log **Breaking Changes** - - Add ``docs/design/`` contributor documentation: ``rules.md`` (the parser's normative rules, with executable examples), ``decisions.md`` (the decision record) and ``mechanisms.md`` (the solution-pattern catalog). New tests execute every documented example and verify every code citation - - Change every vocabulary set in ``nameparser.config`` to a ``frozenset``. Editing one in place -- ``TITLES.add("dean")``, the old way of changing a global default -- now raises ``AttributeError`` at the line that writes it. It was never a reliable way to change a default: whether an edit reached a given parse depended on which config objects had already been built, so one program could hold two disagreeing defaults with nothing to say so. To change the defaults for ``HumanName``, build a private ``Constants`` and pass it (``c = Constants(); c.titles.add("dean"); HumanName(name, constants=c)``); for the 2.0 API, build a lexicon (``Parser(lexicon=Lexicon.default().add(titles={"dean"}))``). Mutating the shared ``CONSTANTS`` still works, but warns and goes away in 3.0. ``CAPITALIZATION_EXCEPTIONS`` is a mapping, not a set, and is unchanged. See :doc:`migrate` and :doc:`customize` (#293) + - **Add docs/design/ contributor documentation:** ``rules.md`` (the parser's normative rules, with executable examples), ``decisions.md`` (the decision record) and ``mechanisms.md`` (the solution-pattern catalog). New tests execute every documented example and verify every code citation + - **Change every vocabulary set in nameparser.config to a frozenset.** Editing one in place -- ``TITLES.add("dean")``, the old way of changing a global default -- now raises ``AttributeError`` at the line that writes it. It was never a reliable way to change a default: whether an edit reached a given parse depended on which config objects had already been built, so one program could hold two disagreeing defaults with nothing to say so. To change the defaults for ``HumanName``, build a private ``Constants`` and pass it (``c = Constants(); c.titles.add("dean"); HumanName(name, constants=c)``); for the 2.0 API, build a lexicon (``Parser(lexicon=Lexicon.default().add(titles={"dean"}))``). Mutating the shared ``CONSTANTS`` still works, but warns and goes away in 3.0. ``CAPITALIZATION_EXCEPTIONS`` is a mapping, not a set, and is unchanged. See :doc:`migrate` and :doc:`customize` (#293) **Behavior Changes** - - Fix a title changing how the name behind it is read. ``"Dr. Van Johnson"`` gave family ``Van Johnson`` with no given name, and ``"Sir Van Johnson"`` gave given ``Van Johnson`` with no family at all; both now read given ``Van``, family ``Johnson`` -- the reading the untitled ``"Van Johnson"`` has always had. A leading word that is *both* a title and a particle is unchanged: ``"St John Smith"``, ``"Do John Smith"`` and ``"Freiherr von Richthofen"`` keep their readings. This fixes a defect v1 shipped as a known-failing test for the life of the 1.x line. See the ``P2`` entry of ``docs/design/decisions.md`` (closes #367) + - **Fix a title changing how the name behind it is read.** ``"Dr. Van Johnson"`` gave family ``Van Johnson`` with no given name, and ``"Sir Van Johnson"`` gave given ``Van Johnson`` with no family at all; both now read given ``Van``, family ``Johnson`` -- the reading the untitled ``"Van Johnson"`` has always had. A leading word that is *both* a title and a particle is unchanged: ``"St John Smith"``, ``"Do John Smith"`` and ``"Freiherr von Richthofen"`` keep their readings. This fixes a defect v1 shipped as a known-failing test for the life of the 1.x line. See the ``P2`` entry of ``docs/design/decisions.md`` (closes #367) - - Fix a given-name title keeping a bound given name from joining the word after it. ``"Sheik abdul salam"`` read given ``abdul``, family ``salam``, and now reads given ``abdul salam`` with an empty family, as ``"Sir John"`` does; ``"الشيخ عبد الله"`` reads given ``عبد الله``. A title that addresses by family is unchanged (``"Dr. abdul salam"``). This also restores ``"Sheik Abu Bakar"`` to given ``Abu Bakar``, which the fix above had regressed, and drops the ``PARTICLE_OR_GIVEN`` ambiguity that name reported through 2.1 (closes #369) + - **Fix a given-name title keeping a bound given name from joining the word after it.** ``"Sheik abdul salam"`` read given ``abdul``, family ``salam``, and now reads given ``abdul salam`` with an empty family, as ``"Sir John"`` does; ``"الشيخ عبد الله"`` reads given ``عبد الله``. A title that addresses by family is unchanged (``"Dr. abdul salam"``). This also restores ``"Sheik Abu Bakar"`` to given ``Abu Bakar``, which the fix above had regressed, and drops the ``PARTICLE_OR_GIVEN`` ambiguity that name reported through 2.1 (closes #369) - - Fix a bound given name swallowing the family name before a single-letter generational suffix. ``"abdul Smith V"`` read given ``abdul Smith`` with no family, where ``"abdul Smith II"`` and ``"abdul Smith Jr"`` read correctly; it now reads given ``abdul``, family ``Smith``, suffix ``V``, and so do ``I`` and ``X``, for every bound given-name word. A suffix word before the numeral no longer hides it (``"abdul Smith Jr V"`` reads family ``Smith``). Shipped since 1.x: 1.4.0 read first ``abdul Smith``, last ``V`` (closes #401) + - **Fix a bound given name swallowing the family name before a single-letter generational suffix.** ``"abdul Smith V"`` read given ``abdul Smith`` with no family, where ``"abdul Smith II"`` and ``"abdul Smith Jr"`` read correctly; it now reads given ``abdul``, family ``Smith``, suffix ``V``, and so do ``I`` and ``X``, for every bound given-name word. A suffix word before the numeral no longer hides it (``"abdul Smith Jr V"`` reads family ``Smith``). Shipped since 1.x: 1.4.0 read first ``abdul Smith``, last ``V`` (closes #401) - - Fix a bound given name joining a suffix as "the word after it". ``"abdul Jr Smith Berg"`` read given ``abdul Jr`` and now reads given ``abdul``, middle ``Jr Smith``, where ``"John Jr Smith Berg"`` puts it. Where the suffix was a split credential the bound word joined *into* it -- ``"abdul Ph. D. Smith Berg"`` read suffix ``abdul Ph. D.``, a 2.0 regression -- and now reads given ``abdul``, middle ``Smith``, suffix ``Ph. D.`` (closes #421) + - **Fix a bound given name joining a suffix as "the word after it".** ``"abdul Jr Smith Berg"`` read given ``abdul Jr`` and now reads given ``abdul``, middle ``Jr Smith``, where ``"John Jr Smith Berg"`` puts it. Where the suffix was a split credential the bound word joined *into* it -- ``"abdul Ph. D. Smith Berg"`` read suffix ``abdul Ph. D.``, a 2.0 regression -- and now reads given ``abdul``, middle ``Smith``, suffix ``Ph. D.`` (closes #421) - - Fix a bound given name joining past a credential that the suffix rule then takes, leaving no family. ``"abdul Smith Jr Ma"`` read given ``abdul Smith`` with no family and now reads family ``Smith``, suffix ``Jr, Ma``, as ``"John Smith Jr Ma"`` does; ``"abdul Smith Ma"`` reads family ``Smith``, suffix ``Ma``. Both as 1.4.0 read them. ``"abdul Smith Berg Ma"`` keeps its join, and ``"Berg, abdul Sir"`` still reads given ``abdul Sir`` (closes #425) + - **Fix a bound given name joining past a credential that the suffix rule then takes, leaving no family.** ``"abdul Smith Jr Ma"`` read given ``abdul Smith`` with no family and now reads family ``Smith``, suffix ``Jr, Ma``, as ``"John Smith Jr Ma"`` does; ``"abdul Smith Ma"`` reads family ``Smith``, suffix ``Ma``. Both as 1.4.0 read them. ``"abdul Smith Berg Ma"`` keeps its join, and ``"Berg, abdul Sir"`` still reads given ``abdul Sir`` (closes #425) - - Remove the Czech/Slovak abbreviation ``roz.`` from the default maiden markers, where it had been eating English given names. Marker matching is case-folded and period-insensitive, so ``Roz`` -- the diminutive of Rosalind -- was the same string as the marker, and a marker takes every word after it: ``"Rosalind Roz Smith"`` read maiden ``Smith`` with **no family name at all**. It and ``"Rosalind Roz Jones Smith"`` now read as 1.4.0 read them. What stops working is the abbreviation itself, for Czech and Slovak callers: ``"Anna Nováková roz. Svobodová"`` reads middle ``Nováková roz.``, family ``Svobodová``, which is also 1.4.0's reading. The full participle is untouched -- ``"Anna Nováková rozená Svobodová"`` still reads maiden ``Svobodová`` -- and a caller who wants the abbreviation back adds it to their own lexicon: ``Parser(lexicon=Lexicon.default().add(maiden_markers={"roz"}))`` (found in #335's review) + - **Remove the Czech/Slovak abbreviation roz. from the default maiden markers, where it had been eating English given names.** Marker matching is case-folded and period-insensitive, so ``Roz`` -- the diminutive of Rosalind -- was the same string as the marker, and a marker takes every word after it: ``"Rosalind Roz Smith"`` read maiden ``Smith`` with **no family name at all**. It and ``"Rosalind Roz Jones Smith"`` now read as 1.4.0 read them. What stops working is the abbreviation itself, for Czech and Slovak callers: ``"Anna Nováková roz. Svobodová"`` reads middle ``Nováková roz.``, family ``Svobodová``, which is also 1.4.0's reading. The full participle is untouched -- ``"Anna Nováková rozená Svobodová"`` still reads maiden ``Svobodová`` -- and a caller who wants the abbreviation back adds it to their own lexicon: ``Parser(lexicon=Lexicon.default().add(maiden_markers={"roz"}))`` (found in #335's review) - - Add the Polish maiden marker ``z domu`` to the default vocabulary, and let a ``maiden_markers`` entry be more than one word. ``"Maria Kowalska z domu Nowak"`` now reads family ``Kowalska``, maiden ``Nowak``, where every earlier version read the marker as part of the name (1.4.0: middle ``Kowalska z domu``, family ``Nowak``). The bracketed spelling moves with it, while a clause holding nothing but the marker stays a nickname (``"Maria Kowalska (z domu)"``). **If you worked around the old limit by storing ``z`` and ``domu`` as separate entries, remove both** -- a bare preposition is then a marker, and a marker takes every word after it, so ``"Anna z Nowak"`` loses its surname. ``maiden_markers`` and ``given_name_titles`` are now the two fields exempt from the multi-word warning. See :doc:`customize` (#434) + - **Add the Polish maiden marker z domu to the default vocabulary, and let a maiden_markers entry be more than one word.** ``"Maria Kowalska z domu Nowak"`` now reads family ``Kowalska``, maiden ``Nowak``, where every earlier version read the marker as part of the name (1.4.0: middle ``Kowalska z domu``, family ``Nowak``). The bracketed spelling moves with it, while a clause holding nothing but the marker stays a nickname (``"Maria Kowalska (z domu)"``). **If you worked around the old limit by storing z and domu as separate entries, remove both** -- a bare preposition is then a marker, and a marker takes every word after it, so ``"Anna z Nowak"`` loses its surname. ``maiden_markers`` and ``given_name_titles`` are now the two fields exempt from the multi-word warning. See :doc:`customize` (#434) - - Fix a bracketed maiden clause reading as a nickname because its brackets were not declared. ``"Jane Smith nee Jones"`` gave maiden ``Jones`` while ``"Jane Smith (née Jones)"`` gave nickname ``née Jones``; the bracketed spelling now reads family ``Smith``, maiden ``Jones`` too, and so does the Japanese ``"山田 花子(旧姓 佐藤)"``, which needed ``Policy(maiden_delimiters=...)`` through 2.1. Every delimiter pair the parser ships moves the same way, quotes included. An interior clause no longer eats the name behind it (``"Jane (née Jones) Smith"`` keeps family ``Smith``), and two clauses beside each other each keep their own role (``"Jane "Janey" Smith (née Jones)"`` reads nickname ``Janey``, maiden ``Jones``). What does NOT move: a lone marker in brackets is just a word, so ``"Jane Smith (née)"`` stays a nickname, and a markerless clause stays a nickname too -- which is what ``Policy(maiden_delimiters=...)`` remains for. This reaches ``HumanName`` (closes #335) + - **Fix a bracketed maiden clause reading as a nickname because its brackets were not declared.** ``"Jane Smith nee Jones"`` gave maiden ``Jones`` while ``"Jane Smith (née Jones)"`` gave nickname ``née Jones``; the bracketed spelling now reads family ``Smith``, maiden ``Jones`` too, and so does the Japanese ``"山田 花子(旧姓 佐藤)"``, which needed ``Policy(maiden_delimiters=...)`` through 2.1. Every delimiter pair the parser ships moves the same way, quotes included. An interior clause no longer eats the name behind it (``"Jane (née Jones) Smith"`` keeps family ``Smith``), and two clauses beside each other each keep their own role (``"Jane "Janey" Smith (née Jones)"`` reads nickname ``Janey``, maiden ``Jones``). What does NOT move: a lone marker in brackets is just a word, so ``"Jane Smith (née)"`` stays a nickname, and a markerless clause stays a nickname too -- which is what ``Policy(maiden_delimiters=...)`` remains for. This reaches ``HumanName`` (closes #335) - - Fix a particle chain and a maiden name taking a trailing generational numeral as a name word. ``"John van der Berg V"`` read family ``van der Berg V`` and ``"John née Jones Smith V"`` read maiden ``Jones Smith V``, where ``"John Smith V"`` reads suffix ``V``; both now stop before the numeral, for ``I`` and ``X`` alike. A word before the numeral that is an initial keeps its reading (``"John van der J. V"``). The chain also stops before a bare credential with words to spare -- ``"John van der Berg Ma"`` reads suffix ``Ma``, as 1.4.0 did -- and no longer swallows the given name behind an unlisted abbreviation: ``"Xyz. van Johnson"`` and ``"Esq. van Gogh"`` read given ``van`` (closes #424) + - **Fix a particle chain and a maiden name taking a trailing generational numeral as a name word.** ``"John van der Berg V"`` read family ``van der Berg V`` and ``"John née Jones Smith V"`` read maiden ``Jones Smith V``, where ``"John Smith V"`` reads suffix ``V``; both now stop before the numeral, for ``I`` and ``X`` alike. A word before the numeral that is an initial keeps its reading (``"John van der J. V"``). The chain also stops before a bare credential with words to spare -- ``"John van der Berg Ma"`` reads suffix ``Ma``, as 1.4.0 did -- and no longer swallows the given name behind an unlisted abbreviation: ``"Xyz. van Johnson"`` and ``"Esq. van Gogh"`` read given ``van`` (closes #424) - - Fix a name losing its given/family split when a comma is followed only by an honorific. ``"John Smith, Mr."`` returned the whole of ``"John Smith"`` as the family name and now gives given ``John``, family ``Smith``, title ``Mr.``: it is ``"Mr. John Smith"`` with the honorific moved to the end, and marks no surname boundary. A comma followed by an actual name still fixes the family (``"John Smith, Jones"``), and a single pre-comma piece has no split to keep (``"Smith, Dr."`` is unchanged). The pre-comma name now also picks up the declared name order -- ``"de Mesnil Juan, Dr."`` keeps family ``de Mesnil`` under a family-first order -- and the particle-or-given ambiguity report (``"Van Johnson, Mr."``) + - **Fix a name losing its given/family split when a comma is followed only by an honorific.** ``"John Smith, Mr."`` returned the whole of ``"John Smith"`` as the family name and now gives given ``John``, family ``Smith``, title ``Mr.``: it is ``"Mr. John Smith"`` with the honorific moved to the end, and marks no surname boundary. A comma followed by an actual name still fixes the family (``"John Smith, Jones"``), and a single pre-comma piece has no split to keep (``"Smith, Dr."`` is unchanged). The pre-comma name now also picks up the declared name order -- ``"de Mesnil Juan, Dr."`` keeps family ``de Mesnil`` under a family-first order -- and the particle-or-given ambiguity report (``"Van Johnson, Mr."``) - - Fix pure postnominals being claimed as titles: ``jr``, ``junior``, ``phd``, ``do`` and ``se`` have left the default ``titles`` vocabulary, and ``dr``/``sra`` have left the suffix vocabulary they never belonged in, so ``"Smith, PhD"`` gives suffix rather than title ``PhD``. All nineteen words in the ``titles``/suffix overlap were given a positional class; twelve are genuine duals and keep both memberships, with position deciding -- ``"Lt. Smith"`` is a title, ``"Smith, LT"`` a postnominal, bare ``Md`` before a name the Bengali and South Asian abbreviation of Muhammad, ``MD`` after it the degree. The cost is in leading position, where a dropped word now reads as a name: ``"PhD Smith"`` gives given ``PhD``, which is what makes ``"Do Nguyen"`` parse as the Vietnamese name it is. ``dr`` and ``sra`` also stop being recognized in trailing position, so ``"John Smith Dr."`` gives family ``Dr.`` as ``"John Smith Prof."`` always has. An ambiguous credential acronym (``ma``, ``ed``, ``jd``, ``do``) counts as a suffix only when written with its periods, so ``"Jack Ma."`` keeps family ``Ma.`` as 1.4.0 read it. Routing a trailing title word to ``title`` is a separate open question (#316) + - **Fix pure postnominals being claimed as titles:** ``jr``, ``junior``, ``phd``, ``do`` and ``se`` have left the default ``titles`` vocabulary, and ``dr``/``sra`` have left the suffix vocabulary they never belonged in, so ``"Smith, PhD"`` gives suffix rather than title ``PhD``. All nineteen words in the ``titles``/suffix overlap were given a positional class; twelve are genuine duals and keep both memberships, with position deciding -- ``"Lt. Smith"`` is a title, ``"Smith, LT"`` a postnominal, bare ``Md`` before a name the Bengali and South Asian abbreviation of Muhammad, ``MD`` after it the degree. The cost is in leading position, where a dropped word now reads as a name: ``"PhD Smith"`` gives given ``PhD``, which is what makes ``"Do Nguyen"`` parse as the Vietnamese name it is. ``dr`` and ``sra`` also stop being recognized in trailing position, so ``"John Smith Dr."`` gives family ``Dr.`` as ``"John Smith Prof."`` always has. An ambiguous credential acronym (``ma``, ``ed``, ``jd``, ``do``) counts as a suffix only when written with its periods, so ``"Jack Ma."`` keeps family ``Ma.`` as 1.4.0 read it. Routing a trailing title word to ``title`` is a separate open question (#316) - - Fix a credential run after a one-word family comma reading as a title or a given name. ``"Smith, Jr."`` and ``"Smith, PhD"`` now give suffix ``Jr.``/``PhD`` where they gave title, and ``"Smith, Ph. D. Jr."`` gives suffix ``Ph. D. Jr.`` where the split credential landed in the given name -- a regression from 1.4.0. The position right after a family comma is postnominal position. Vocabulary still decides which words qualify (``"Smith, Dr."`` keeps title ``Dr.``), the leading readings are untouched (``"Sr. Garcia"`` is still title ``Sr.``), and a name word in the run makes it the given-and-suffix reading it always had (``"Smith, John Jr."``) (closes #296, closes #325) + - **Fix a credential run after a one-word family comma reading as a title or a given name.** ``"Smith, Jr."`` and ``"Smith, PhD"`` now give suffix ``Jr.``/``PhD`` where they gave title, and ``"Smith, Ph. D. Jr."`` gives suffix ``Ph. D. Jr.`` where the split credential landed in the given name -- a regression from 1.4.0. The position right after a family comma is postnominal position. Vocabulary still decides which words qualify (``"Smith, Dr."`` keeps title ``Dr.``), the leading readings are untouched (``"Sr. Garcia"`` is still title ``Sr.``), and a name word in the run makes it the given-and-suffix reading it always had (``"Smith, John Jr."``) (closes #296, closes #325) - - Fix a space-separated credential run after a family comma rendering with a comma the name never had. ``"Smith, MD PhD"`` gives suffix ``MD PhD`` where it gave ``MD, PhD``, and ``"Smith, CBE MC"``, ``"Smith, BSc MBA"`` and ``"Smith, Dr. MD PhD"`` the same. The roles are unchanged; only the rendered string carried the extra comma. This reaches any family comma whose following segment holds no name word, not only a one-word family, so ``"John Smith, Jr. III"`` gives suffix ``Jr. III`` -- also what 1.4.0 gave. A run written with commas keeps them (``"Smith, MD, PhD"``), and a name written without a comma is unaffected and still renders its run comma-joined, so re-parsing ``str()`` output does not reproduce the run (closes #429) + - **Fix a space-separated credential run after a family comma rendering with a comma the name never had.** ``"Smith, MD PhD"`` gives suffix ``MD PhD`` where it gave ``MD, PhD``, and ``"Smith, CBE MC"``, ``"Smith, BSc MBA"`` and ``"Smith, Dr. MD PhD"`` the same. The roles are unchanged; only the rendered string carried the extra comma. This reaches any family comma whose following segment holds no name word, not only a one-word family, so ``"John Smith, Jr. III"`` gives suffix ``Jr. III`` -- also what 1.4.0 gave. A run written with commas keeps them (``"Smith, MD, PhD"``), and a name written without a comma is unaffected and still renders its run comma-joined, so re-parsing ``str()`` output does not reproduce the run (closes #429) - - Fix a one-character suffix word after a comma being read by the wrong neighbour. ``"Smith, PSM I"`` gives suffix ``PSM I`` where it gave given ``PSM`` and suffix ``I``, and ``"Smith, John V."`` gives middle ``V.`` where it gave suffix ``V.``. Inside a comma part a suffix word short enough to be mistaken for an initial -- ``I``, ``V`` and ``2`` in the shipped vocabulary -- is read by what stands before it: behind a credential it describes that credential (``PSM I`` is Professional Scrum Master level I), and behind a name a period marks an abbreviation and so a middle initial. A numeral written bare after a name is still the generation it looks like (``"Smith, John V"`` is suffix ``V``), and a name with no comma is untouched (closes #430, closes #432) + - **Fix a one-character suffix word after a comma being read by the wrong neighbour.** ``"Smith, PSM I"`` gives suffix ``PSM I`` where it gave given ``PSM`` and suffix ``I``, and ``"Smith, John V."`` gives middle ``V.`` where it gave suffix ``V.``. Inside a comma part a suffix word short enough to be mistaken for an initial -- ``I``, ``V`` and ``2`` in the shipped vocabulary -- is read by what stands before it: behind a credential it describes that credential (``PSM I`` is Professional Scrum Master level I), and behind a name a period marks an abbreviation and so a middle initial. A numeral written bare after a name is still the generation it looks like (``"Smith, John V"`` is suffix ``V``), and a name with no comma is untouched (closes #430, closes #432) - - Fix a name opening with a particle that is *never* a given name being split at the particle under a family-first name order. ``"de Mesnil"`` read as family ``de``, given ``Mesnil`` and ``"de la Vega"`` as family ``de``, given ``la Vega``; each is now the whole surname, as it has always been in the default order, under ``FAMILY_FIRST`` and ``FAMILY_FIRST_GIVEN_LAST`` alike. A word that can never be a given name leaves ``name_order`` nothing to decide. Standing alone is the whole of it: ``"Juan de la Vega"`` under ``FAMILY_FIRST`` still reports given ``de la Vega``. A leading particle that *may* be a given name is genuinely order-dependent and is untouched, so ``"van Gogh"`` still reads family ``van``, given ``Gogh`` under both family-first orders (closes #359) + - **Fix a name opening with a particle that is never a given name being split at the particle under a family-first name order.** ``"de Mesnil"`` read as family ``de``, given ``Mesnil`` and ``"de la Vega"`` as family ``de``, given ``la Vega``; each is now the whole surname, as it has always been in the default order, under ``FAMILY_FIRST`` and ``FAMILY_FIRST_GIVEN_LAST`` alike. A word that can never be a given name leaves ``name_order`` nothing to decide. Standing alone is the whole of it: ``"Juan de la Vega"`` under ``FAMILY_FIRST`` still reports given ``de la Vega``. A leading particle that *may* be a given name is genuinely order-dependent and is untouched, so ``"van Gogh"`` still reads family ``van``, given ``Gogh`` under both family-first orders (closes #359) - - Fix a family name made only of particle words reporting no base, so the surname vanished from ``family_base`` and from the initials. ``parse("Anh Do")`` gave family ``Do`` with ``family_base`` ``''`` and initials ``A.``, and under ``Policy(name_order=FAMILY_FIRST)`` ``"Del Toro"`` gave family ``Del`` the same way. A particle standing alone in a name part is not doing a particle's work there and now reads as an ordinary name word: ``"Anh Do"`` is base ``Do``, initials ``A. D.``; ``"Juan van der"`` is base ``van der``, initials ``J. v. d.``; ``"Nguyen, Van Le"`` initials ``V. L. N.`` where the middle name used to be dropped. Where the particles DO join a name word nothing changes (``"Juan de la Vega"`` keeps base ``Vega``). The parse fields themselves do not move -- only the derived views and the initials. See the ``R2`` entry of ``docs/design/decisions.md`` (closes #385, closes #402) + - **Fix a family name made only of particle words reporting no base, so the surname vanished from family_base and from the initials.** ``parse("Anh Do")`` gave family ``Do`` with ``family_base`` ``''`` and initials ``A.``, and under ``Policy(name_order=FAMILY_FIRST)`` ``"Del Toro"`` gave family ``Del`` the same way. A particle standing alone in a name part is not doing a particle's work there and now reads as an ordinary name word: ``"Anh Do"`` is base ``Do``, initials ``A. D.``; ``"Juan van der"`` is base ``van der``, initials ``J. v. d.``; ``"Nguyen, Van Le"`` initials ``V. L. N.`` where the middle name used to be dropped. Where the particles DO join a name word nothing changes (``"Juan de la Vega"`` keeps base ``Vega``). The parse fields themselves do not move -- only the derived views and the initials. See the ``R2`` entry of ``docs/design/decisions.md`` (closes #385, closes #402) - - Fix case repair lowercasing the words of a family name made only of particle words, where every other view already reads them as ordinary name words. ``HumanName("ANH DO").capitalize()`` gives ``Anh Do`` where it gave ``Anh do``, and ``"anh van do"`` gives ``Anh Van Do``. The test is the whole part, not a particle standing alone, which is why the two-word family moves along with the one-word one. This DIFFERS FROM 1.4.0 deliberately and does not restore it: 1.4.0 returned ``Anh do``. The accepted cost is that a family which is nothing but particles capitalizes too, so ``"juan van der"`` gives ``Juan Van Der``. A conjunction is untouched (``"der, y van"`` gives ``y Van Der``), and where the particles DO join a name word nothing changes (``"juan de la vega"`` still gives ``Juan de la Vega``) (closes #407) + - **Fix case repair lowercasing the words of a family name made only of particle words, where every other view already reads them as ordinary name words.** ``HumanName("ANH DO").capitalize()`` gives ``Anh Do`` where it gave ``Anh do``, and ``"anh van do"`` gives ``Anh Van Do``. The test is the whole part, not a particle standing alone, which is why the two-word family moves along with the one-word one. This DIFFERS FROM 1.4.0 deliberately and does not restore it: 1.4.0 returned ``Anh do``. The accepted cost is that a family which is nothing but particles capitalizes too, so ``"juan van der"`` gives ``Juan Van Der``. A conjunction is untouched (``"der, y van"`` gives ``y Van Der``), and where the particles DO join a name word nothing changes (``"juan de la vega"`` still gives ``Juan de la Vega``) (closes #407) - - Change case repair to read the parser's own ``conjunction`` tag instead of re-deciding, from the word's spelling, whether a word is a conjunction or an initial. Two spellings of one name disagreed because of it: ``"juan e-f smith"`` capitalized to ``Juan e-F Smith`` while ``"JUAN E-F SMITH"`` gave ``Juan E-F Smith``; both give ``Juan E-F Smith`` now. A conjunction written as a word of its own is untouched (``"juan y garcia"`` still repairs to ``Juan y Garcia``). A field assigned after the parse was never classified, so repair asks the vocabulary there -- today's vocabulary, which is narrower than 1.4.0 parity: ``h.last = "хосе и мария сантос"`` gives ``Хосе И Мария Сантос`` on 1.4.0 and ``Хосе и Мария Сантос`` here. One reading changes for hand-built ``Token``\ s in the 2.0 API: an untagged token whose text is conjunction vocabulary now capitalizes as an ordinary name word. See the ``R4`` entry of ``docs/design/decisions.md`` (closes #458) + - **Change case repair to read the parser's own conjunction tag instead of re-deciding, from the word's spelling, whether a word is a conjunction or an initial.** Two spellings of one name disagreed because of it: ``"juan e-f smith"`` capitalized to ``Juan e-F Smith`` while ``"JUAN E-F SMITH"`` gave ``Juan E-F Smith``; both give ``Juan E-F Smith`` now. A conjunction written as a word of its own is untouched (``"juan y garcia"`` still repairs to ``Juan y Garcia``). A field assigned after the parse was never classified, so repair asks the vocabulary there -- today's vocabulary, which is narrower than 1.4.0 parity: ``h.last = "хосе и мария сантос"`` gives ``Хосе И Мария Сантос`` on 1.4.0 and ``Хосе и Мария Сантос`` here. One reading changes for hand-built ``Token``\ s in the 2.0 API: an untagged token whose text is conjunction vocabulary now capitalizes as an ordinary name word. See the ``R4`` entry of ``docs/design/decisions.md`` (closes #458) - - Change the parse-cost benchmark to bound function calls per parse rather than wall-clock seconds. The two ``under_a_second`` tests asserted that 1000 parses take under a second; on CI that bound failed four times across two branches at 1.01 to 1.08 seconds while the same code re-ran green on master. Frame counts do not move under load, so growth shows up in a diff instead of in a threshold that happens to break. The bound is a per-interpreter band of ±2%, with a loose five-second backstop over both entry points for the class frame counts cannot see. Every figure is recomputable with ``uv run python tools/perf/call_count.py --against v2.1.0``; the counts and the per-PR attribution are in the ``parse-cost`` entry of ``docs/design/decisions.md``. No user-visible behavior changes (closes #475) + - **Change the parse-cost benchmark to bound function calls per parse rather than wall-clock seconds.** The two ``under_a_second`` tests asserted that 1000 parses take under a second; on CI that bound failed four times across two branches at 1.01 to 1.08 seconds while the same code re-ran green on master. Frame counts do not move under load, so growth shows up in a diff instead of in a threshold that happens to break. The bound is a per-interpreter band of ±2%, with a loose five-second backstop over both entry points for the class frame counts cannot see. Every figure is recomputable with ``uv run python tools/perf/call_count.py --against v2.1.0``; the counts and the per-PR attribution are in the ``parse-cost`` entry of ``docs/design/decisions.md``. No user-visible behavior changes (closes #475) - - Fix a name that opens with a spaced ``Ph. D.`` losing its surname. ``parse("Ph. D. Van Johnson")`` read given ``Van Johnson`` with an empty ``family`` and suffix ``Ph. D.``; it now reads title ``Ph.``, given ``D.``, family ``Van Johnson``. A suffix never begins a name, and every other suffix-shaped word standing first already fell out as a title (``Jr.``, ``MD``, ``Esq.``) or as an ordinary name word (``PhD``, ``III``), so the split credential was the only shape that reached the defect. A family comma still opens a listing rather than a name (``"John Smith Ph. D."`` and ``"Smith, Ph. D. Jr."`` keep their suffixes), and "the head" means the head of the string rather than of the name, so ``"Sir Ph. D. Van Johnson"`` is unchanged. This RESTORES 1.4.0. One accepted consequence: ``Parser.revise(suffix="Ph. D.")`` renders ``Ph., D.`` (closes #371) + - **Fix a name that opens with a spaced Ph. D. losing its surname.** ``parse("Ph. D. Van Johnson")`` read given ``Van Johnson`` with an empty ``family`` and suffix ``Ph. D.``; it now reads title ``Ph.``, given ``D.``, family ``Van Johnson``. A suffix never begins a name, and every other suffix-shaped word standing first already fell out as a title (``Jr.``, ``MD``, ``Esq.``) or as an ordinary name word (``PhD``, ``III``), so the split credential was the only shape that reached the defect. A family comma still opens a listing rather than a name (``"John Smith Ph. D."`` and ``"Smith, Ph. D. Jr."`` keep their suffixes), and "the head" means the head of the string rather than of the name, so ``"Sir Ph. D. Van Johnson"`` is unchanged. This RESTORES 1.4.0. One accepted consequence: ``Parser.revise(suffix="Ph. D.")`` renders ``Ph., D.`` (closes #371) - - Fix a trailing surname particle being stranded as a standalone middle name under ``Policy(name_order=FAMILY_FIRST)``, where the same listing written with a comma reads it as part of the surname. ``"Jong Anke de"`` gave family ``Jong`` with ``de`` left as a middle name and now gives family ``de Jong``, given ``Anke`` -- the answer ``parse("Jong, Anke de")`` has always given. The test is the SLOT the particle landed in, not the word, and ``FAMILY_FIRST`` is the only order that puts a trailing piece in a middle; ``FAMILY_FIRST_GIVEN_LAST`` puts it in the given slot, so ``"Nguyen Thi Van"`` under that order still reads given ``Van``. That reads both traditions without asking about the vocabulary: ``"Beethoven Ludwig van"`` under ``FAMILY_FIRST`` now gives family ``van Beethoven``. A particle standing alone in the given slot is no longer folded into the family either, so ``"Ménil de"`` reports given ``de``. Nothing moves under the DEFAULT name order. See the ``P6`` entry of ``docs/design/decisions.md`` (closes #467) + - **Fix a trailing surname particle being stranded as a standalone middle name under a family-first name order.** Under ``Policy(name_order=FAMILY_FIRST)`` the same listing written with a comma reads it as part of the surname: ``"Jong Anke de"`` gave family ``Jong`` with ``de`` left as a middle name and now gives family ``de Jong``, given ``Anke`` -- the answer ``parse("Jong, Anke de")`` has always given. The test is the SLOT the particle landed in, not the word, and ``FAMILY_FIRST`` is the only order that puts a trailing piece in a middle; ``FAMILY_FIRST_GIVEN_LAST`` puts it in the given slot, so ``"Nguyen Thi Van"`` under that order still reads given ``Van``. That reads both traditions without asking about the vocabulary: ``"Beethoven Ludwig van"`` under ``FAMILY_FIRST`` now gives family ``van Beethoven``. A particle standing alone in the given slot is no longer folded into the family either, so ``"Ménil de"`` reports given ``de``. Nothing moves under the DEFAULT name order. See the ``P6`` entry of ``docs/design/decisions.md`` (closes #467) - - Fix ``initials()`` ordering a name differently from the fields of the same parse. Two rules fold words into the family and render them ahead of it -- ``Policy(middle_as_family=True)`` and the tussenvoegsel attachment after a family comma -- and the ``family`` field honored the fold where ``initials()`` did not: ``parse("der, y van")`` gave family ``van der`` but initials ``y. d. v.``, and now gives ``y. v. d.``. Under ``middle_as_family`` this RESTORES v1, that option being ``middle_name_as_last``'s successor: ``"Doe, Dr. John A."`` gives ``J. A. D.`` again where 2.0 through 2.2 gave ``J. D. A.``. ``HumanName.initials()`` was already right and is unchanged. See the ``R3`` entry of ``docs/design/decisions.md`` (closes #408) + - **Fix initials() ordering a name differently from the fields of the same parse.** Two rules fold words into the family and render them ahead of it -- ``Policy(middle_as_family=True)`` and the tussenvoegsel attachment after a family comma -- and the ``family`` field honored the fold where ``initials()`` did not: ``parse("der, y van")`` gave family ``van der`` but initials ``y. d. v.``, and now gives ``y. v. d.``. Under ``middle_as_family`` this RESTORES v1, that option being ``middle_name_as_last``'s successor: ``"Doe, Dr. John A."`` gives ``J. A. D.`` again where 2.0 through 2.2 gave ``J. D. A.``. ``HumanName.initials()`` was already right and is unchanged. See the ``R3`` entry of ``docs/design/decisions.md`` (closes #408) - - Fix a tussenvoegsel attached to the family name after a comma deciding a genuinely uncertain reading and reporting nothing. ``"Van Johnson"`` reports a ``PARTICLE_OR_GIVEN`` ambiguity -- ``Van`` is a Dutch particle and a Vietnamese given name -- while ``"Nguyen, Thi Van"`` picked the same word the same way, silently. The attachment now reports the fork it decides: ``"Nguyen, Thi Van"``, ``"Berg, Jan van der"`` and ``"Vega, Juan de la"`` each gain a ``PARTICLE_OR_GIVEN``, while a particle already read as a post-nominal reports ``SUFFIX_OR_NAME`` instead (``"Berg, Jan vd"``). A particle where nothing was overridden reports nothing, so ``"Jong, Piet de"`` is unchanged. Worth knowing before you filter on this: ``"Beethoven, Ludwig van"`` -- read exactly right -- now carries a report too, nothing in the input separating it from ``"Nguyen, Thi Van"``. ``ambiguities`` is the only value that grows (closes #405) + - **Fix a tussenvoegsel attached to the family name after a comma deciding a genuinely uncertain reading and reporting nothing.** ``"Van Johnson"`` reports a ``PARTICLE_OR_GIVEN`` ambiguity -- ``Van`` is a Dutch particle and a Vietnamese given name -- while ``"Nguyen, Thi Van"`` picked the same word the same way, silently. The attachment now reports the fork it decides: ``"Nguyen, Thi Van"``, ``"Berg, Jan van der"`` and ``"Vega, Juan de la"`` each gain a ``PARTICLE_OR_GIVEN``, while a particle already read as a post-nominal reports ``SUFFIX_OR_NAME`` instead (``"Berg, Jan vd"``). A particle where nothing was overridden reports nothing, so ``"Jong, Piet de"`` is unchanged. Worth knowing before you filter on this: ``"Beethoven, Ludwig van"`` -- read exactly right -- now carries a report too, nothing in the input separating it from ``"Nguyen, Thi Van"``. ``ambiguities`` is the only value that grows (closes #405) - - Fix a tussenvoegsel after a family comma being parsed as a middle name. Dutch and Belgian alphabetized listings move the particle behind the given name -- ``"Beethoven, Ludwig van"`` is how ``"Ludwig van Beethoven"`` is filed -- and it was read as a middle name rather than as part of the surname: ``"Beethoven, Ludwig van"`` gave middle ``van``, last ``Beethoven``, and ``"Berg, Jan van der"`` gave middle ``van der``. Those now read family ``van Beethoven`` and ``van der Berg``, and the derived views follow, so ``family_particles`` is ``van`` and ``family_base`` ``Beethoven``. Two guards bound it: a name whose only given word is the particle keeps it (``"Nguyen, Van"`` still reads given ``Van``), and where the word is BOTH particle and suffix vocabulary the attachment wins, so ``"Berg, Jan vd"`` reads family ``vd Berg`` where 1.4.0 and 2.1 alike gave suffix ``vd`` -- as does ``mc``. Names without the comma are untouched (closes #379, closes #380) + - **Fix a tussenvoegsel after a family comma being parsed as a middle name.** Dutch and Belgian alphabetized listings move the particle behind the given name -- ``"Beethoven, Ludwig van"`` is how ``"Ludwig van Beethoven"`` is filed -- and it was read as a middle name rather than as part of the surname: ``"Beethoven, Ludwig van"`` gave middle ``van``, last ``Beethoven``, and ``"Berg, Jan van der"`` gave middle ``van der``. Those now read family ``van Beethoven`` and ``van der Berg``, and the derived views follow, so ``family_particles`` is ``van`` and ``family_base`` ``Beethoven``. Two guards bound it: a name whose only given word is the particle keeps it (``"Nguyen, Van"`` still reads given ``Van``), and where the word is BOTH particle and suffix vocabulary the attachment wins, so ``"Berg, Jan vd"`` reads family ``vd Berg`` where 1.4.0 and 2.1 alike gave suffix ``vd`` -- as does ``mc``. Names without the comma are untouched (closes #379, closes #380) - - Add ``abd`` to :data:`~nameparser.config.bound_given_names.BOUND_GIVEN_NAMES`, so the spellings that write the article as its own word join like the others do: ``"abd Allah Smith"`` was given ``abd``, middle ``Allah`` and is now given ``abd Allah``. ``abdul``, ``abdel`` and ``abdal`` were already there, and the Arabic-script ``عبد`` has covered the same word since 2.0, so only the Latin spelling was short. The word is also the postnominal ABD ("All But Dissertation") and stays in ``SUFFIX_ACRONYMS``: position tells the two readings apart, so ``"Jane Smith ABD"``, ``"Jane Smith, ABD"`` and ``"Jane Smith A.B.D."`` all still read the credential as a suffix (#400) + - **Add abd to BOUND_GIVEN_NAMES, so the spellings that write the article as its own word join like the others do:** ``"abd Allah Smith"`` was given ``abd``, middle ``Allah`` and is now given ``abd Allah``. ``abdul``, ``abdel`` and ``abdal`` were already there, and the Arabic-script ``عبد`` has covered the same word since 2.0, so only the Latin spelling was short. The word is also the postnominal ABD ("All But Dissertation") and stays in ``SUFFIX_ACRONYMS``: position tells the two readings apart, so ``"Jane Smith ABD"``, ``"Jane Smith, ABD"`` and ``"Jane Smith A.B.D."`` all still read the credential as a suffix (#400) - - Change how far a leading never-given particle takes the surname when a family-first ``name_order`` is declared. ``Policy(name_order=FAMILY_FIRST)`` read ``"de Mesnil Juan"`` as family ``de Mesnil Juan`` -- the whole name -- and now reads family ``de Mesnil``, given ``Juan``. The default order is unchanged, deliberately: with no order declared nothing marks where the surname ends, and a particle followed by several words really can be all surname (``von Bergen Wessels``); a caller who means family ``de la Vega`` plus given ``Juan`` there writes the comma. The run takes one name WORD rather than one token, so the stop cannot land inside a conjunction-joined run or a bound given-name pair: ``"de la Vega y Santos Juan"`` reads family ``de la Vega y Santos``, ``"ibn Awf abdul Rahman"`` given ``abdul Rahman``. Where two or more words are left over, the two family-first orders differ from each other for the first time: ``"de la Cruz Juan Carlos"`` reads given ``Juan``, middle ``Carlos`` under ``FAMILY_FIRST`` and the reverse under ``FAMILY_FIRST_GIVEN_LAST``. An ambiguous leading particle and a family comma are both untouched. See the ``P1`` entry of ``docs/design/decisions.md`` (closes #395) + - **Change how far a leading never-given particle takes the surname when a family-first name_order is declared.** ``Policy(name_order=FAMILY_FIRST)`` read ``"de Mesnil Juan"`` as family ``de Mesnil Juan`` -- the whole name -- and now reads family ``de Mesnil``, given ``Juan``. The default order is unchanged, deliberately: with no order declared nothing marks where the surname ends, and a particle followed by several words really can be all surname (``von Bergen Wessels``); a caller who means family ``de la Vega`` plus given ``Juan`` there writes the comma. The run takes one name WORD rather than one token, so the stop cannot land inside a conjunction-joined run or a bound given-name pair: ``"de la Vega y Santos Juan"`` reads family ``de la Vega y Santos``, ``"ibn Awf abdul Rahman"`` given ``abdul Rahman``. Where two or more words are left over, the two family-first orders differ from each other for the first time: ``"de la Cruz Juan Carlos"`` reads given ``Juan``, middle ``Carlos`` under ``FAMILY_FIRST`` and the reverse under ``FAMILY_FIRST_GIVEN_LAST``. An ambiguous leading particle and a family comma are both untouched. See the ``P1`` entry of ``docs/design/decisions.md`` (closes #395) - - Change the ``detail`` text of a ``PARTICLE_OR_GIVEN`` ambiguity to name the role the leading particle was actually given. It said "read as a given name" under every ``name_order``, which is false under ``Policy(name_order=FAMILY_FIRST)`` -- there ``"Van Johnson"`` reads family ``Van``, given ``Johnson``, and the report described the reading not taken. It now ends "read as a family name" in that case. The ``kind`` is unchanged and stays ``PARTICLE_OR_GIVEN``; only the human-readable text moved, and default-order output is identical (#355) + - **Change the detail text of a PARTICLE_OR_GIVEN ambiguity to name the role the leading particle was actually given.** It said "read as a given name" under every ``name_order``, which is false under ``Policy(name_order=FAMILY_FIRST)`` -- there ``"Van Johnson"`` reads family ``Van``, given ``Johnson``, and the report described the reading not taken. It now ends "read as a family name" in that case. The ``kind`` is unchanged and stays ``PARTICLE_OR_GIVEN``; only the human-readable text moved, and default-order output is identical (#355) - - Fix a maiden name being lost when a particle stood in front of the marker. ``"Ursula Leyen geb. Albrecht"`` reported maiden ``Albrecht`` correctly, but ``"Ursula von der Leyen geb. Albrecht"`` -- the same words one particle chain apart -- gave family ``von der Leyen geb. Albrecht`` and no maiden name at all, as did ``"Jane van der Berg née Jones"``. A suffix already stopped the particle chain; a marker now does too, so those read family ``von der Leyen`` maiden ``Albrecht`` and family ``van der Berg`` maiden ``Jones``. A single leading particle always worked (``"von Müller geb. Schmidt"``); a leading run of two did not. A marker needs a word after it to hand the maiden name to, so a trailing one bounds nothing and ``"Jane van der Berg née"`` keeps family ``van der Berg née`` -- which is what keeps ``"Jane van der Nee"`` intact, ``Nee`` being an attested surname as well as a marker spelling. Under a family-first order ``"de la Cruz née Vega"`` now reads family ``de la Cruz``, maiden ``Vega``. Two limits remain, both recorded in ``rules.md#M2``: a conjunction join and a bound given-name join each still absorb a marker first (closes #399) + - **Fix a maiden name being lost when a particle stood in front of the marker.** ``"Ursula Leyen geb. Albrecht"`` reported maiden ``Albrecht`` correctly, but ``"Ursula von der Leyen geb. Albrecht"`` -- the same words one particle chain apart -- gave family ``von der Leyen geb. Albrecht`` and no maiden name at all, as did ``"Jane van der Berg née Jones"``. A suffix already stopped the particle chain; a marker now does too, so those read family ``von der Leyen`` maiden ``Albrecht`` and family ``van der Berg`` maiden ``Jones``. A single leading particle always worked (``"von Müller geb. Schmidt"``); a leading run of two did not. A marker needs a word after it to hand the maiden name to, so a trailing one bounds nothing and ``"Jane van der Berg née"`` keeps family ``van der Berg née`` -- which is what keeps ``"Jane van der Nee"`` intact, ``Nee`` being an attested surname as well as a marker spelling. Under a family-first order ``"de la Cruz née Vega"`` now reads family ``de la Cruz``, maiden ``Vega``. Two limits remain, both recorded in ``rules.md#M2``: a conjunction join and a bound given-name join each still absorb a marker first (closes #399) - - 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"`` becomes last ``Mc Donald Smith``. The ``PARTICLE_OR_GIVEN`` ambiguity goes with it for ``mc`` and ``ste``, which only changed halves -- ``"Jack Mc Donald"`` 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"`` is now last ``das Neves``. Never-given membership goes from 28 words to 33 (closes #360) + - **Move mc and ste into the never-given half of the particle vocabulary, and add los, las and das.** The Spanish and Portuguese articles 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"`` becomes last ``Mc Donald Smith``. The ``PARTICLE_OR_GIVEN`` ambiguity goes with it for ``mc`` and ``ste``, which only changed halves -- ``"Jack Mc Donald"`` 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"`` is now last ``das Neves``. Never-given membership goes from 28 words to 33 (closes #360) - - 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 reads given ``abdul``, family ``Berg``; it now reads given ``abdul``, family ``Berg``, maiden ``Jones``. The join also declines when the piece it would absorb is a marker, so ``"van der Berg, abdul née Jones"`` reads given ``abdul``, family ``van der Berg``, maiden ``Jones`` where it read given ``abdul née``. A name with a word to spare is untouched (``"abd Allah Smith née Jones"`` still joins). Where the bound word is ALSO suffix vocabulary, a declining join after a family comma leaves the post-nominal reading: ``"Berg, abd née Jones"`` reads family ``Berg``, suffix ``abd``, maiden ``Jones``, as ``"Berg, abd"`` alone always has (closes #411) + - **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 reads given ``abdul``, family ``Berg``; it now reads given ``abdul``, family ``Berg``, maiden ``Jones``. The join also declines when the piece it would absorb is a marker, so ``"van der Berg, abdul née Jones"`` reads given ``abdul``, family ``van der Berg``, maiden ``Jones`` where it read given ``abdul née``. A name with a word to spare is untouched (``"abd Allah Smith née Jones"`` still joins). Where the bound word is ALSO suffix vocabulary, a declining join after a family comma leaves the post-nominal reading: ``"Berg, abd née Jones"`` reads family ``Berg``, suffix ``abd``, maiden ``Jones``, as ``"Berg, abd"`` alone always has (closes #411) - - Fix a maiden clause changing how the rest of the name is read, and a connective join keeping the marker in the surname. A maiden marker and the name it introduces are not part of the name they follow, but the grouping rules that count a name's words counted them anyway: ``"juan y garcia"`` reads given ``juan``, middle ``y``, family ``garcia``, while ``"juan y garcia nee jones"`` read given ``juan y garcia`` with NO family name at all -- eight more corpus names lose the family the same way under an appended clause. A name of two or more name words now reads as it reads without its maiden clause, plus the maiden name. The connective join used to merge the marker into a multi-word piece, so ``"Jane van der Berg née y Jones"`` kept family ``van der Berg née y Jones`` and now reads family ``van der Berg``, maiden ``y Jones``. Two limits: a bound given-name word still never joins onto a marker standing as a word of its own (``"Berg, abdul née PhD"``), and a suffix-vocabulary word inside the maiden name stops the marker, so ``"Jane née Jr y Jones"`` now reads family ``Jr y Jones`` with no maiden name (closes #412, closes #417, closes #418) + - **Fix a maiden clause changing how the rest of the name is read, and a connective join keeping the marker in the surname.** A maiden marker and the name it introduces are not part of the name they follow, but the grouping rules that count a name's words counted them anyway: ``"juan y garcia"`` reads given ``juan``, middle ``y``, family ``garcia``, while ``"juan y garcia nee jones"`` read given ``juan y garcia`` with NO family name at all -- eight more corpus names lose the family the same way under an appended clause. A name of two or more name words now reads as it reads without its maiden clause, plus the maiden name. The connective join used to merge the marker into a multi-word piece, so ``"Jane van der Berg née y Jones"`` kept family ``van der Berg née y Jones`` and now reads family ``van der Berg``, maiden ``y Jones``. Two limits: a bound given-name word still never joins onto a marker standing as a word of its own (``"Berg, abdul née PhD"``), and a suffix-vocabulary word inside the maiden name stops the marker, so ``"Jane née Jr y Jones"`` now reads family ``Jr y Jones`` with no maiden name (closes #412, closes #417, closes #418) - - Fix a title-plus-surname name losing its family name whenever anything stood beside it. ``"Dr. Smith"`` reads family ``Smith``, but ``"Dr. Smith née Jones"`` read given ``Smith`` with no family at all, and so did ``"Dr. Smith PhD"`` and ``"Dr. "Smitty" Smith"``. A suffix, a nickname and a maiden name each stand beside the name rather than in it, and the rule now counts name words alone: those read family ``Smith`` with maiden ``Jones``, suffix ``PhD`` and nickname ``Smitty`` respectively, and ``"Freiherr von Richthofen geb. Albrecht"`` reads family ``von Richthofen``, maiden ``Albrecht``. A given-name title still names no family: ``"Sir John née Jones"`` keeps given ``John``, exactly as ``"Sir John"`` does. One name moves where the nickname LEADS: ``"'Smitty' Dr. Jones"`` reads family ``Jones`` where it read given (closes #410) + - **Fix a title-plus-surname name losing its family name whenever anything stood beside it.** ``"Dr. Smith"`` reads family ``Smith``, but ``"Dr. Smith née Jones"`` read given ``Smith`` with no family at all, and so did ``"Dr. Smith PhD"`` and ``"Dr. "Smitty" Smith"``. A suffix, a nickname and a maiden name each stand beside the name rather than in it, and the rule now counts name words alone: those read family ``Smith`` with maiden ``Jones``, suffix ``PhD`` and nickname ``Smitty`` respectively, and ``"Freiherr von Richthofen geb. Albrecht"`` reads family ``von Richthofen``, maiden ``Albrecht``. A given-name title still names no family: ``"Sir John née Jones"`` keeps given ``John``, exactly as ``"Sir John"`` does. One name moves where the nickname LEADS: ``"'Smitty' Dr. Jones"`` reads family ``Jones`` where it read given (closes #410) - - Fix a name that is a surname and a maiden clause reporting no family at all. ``"Smith née Jones"`` read given ``Smith`` with an empty ``family``; it now reads family ``Smith``, maiden ``Jones``. A maiden marker announces a FORMER surname, and a former surname only means something beside a current one, so the single name word left standing is the surname the bearer uses now. Every spelling of the shape moves, including a bracket pair you declared to mean maiden: under ``Policy(maiden_delimiters=frozenset({("(", ")")}))``, ``"Smith (Jones)"`` reads family ``Smith``, maiden ``Jones``. ``"Smith (née Jones)"`` RESTORES 1.4.0, which read family ``Smith`` with the clause as a nickname. An annotation beside the name is no part of it, so ``"Smith née Jones PhD"`` reads family ``Smith``, suffix ``PhD``. Two shapes deliberately do NOT move: a word the vocabulary claims as a given name (``"abd née Jones"``) and a word written as an initial (``"J. née Jones Smith V"``) -- this rule changes what POSITION decided and does not reach what a word already is. **If you have code that reads the lone name word beside a maiden clause out of ``given``, this is the release where it moves to ``family``** (closes #445) + - **Fix a name that is a surname and a maiden clause reporting no family at all.** ``"Smith née Jones"`` read given ``Smith`` with an empty ``family``; it now reads family ``Smith``, maiden ``Jones``. A maiden marker announces a FORMER surname, and a former surname only means something beside a current one, so the single name word left standing is the surname the bearer uses now. Every spelling of the shape moves, including a bracket pair you declared to mean maiden: under ``Policy(maiden_delimiters=frozenset({("(", ")")}))``, ``"Smith (Jones)"`` reads family ``Smith``, maiden ``Jones``. ``"Smith (née Jones)"`` RESTORES 1.4.0, which read family ``Smith`` with the clause as a nickname. An annotation beside the name is no part of it, so ``"Smith née Jones PhD"`` reads family ``Smith``, suffix ``PhD``. Two shapes deliberately do NOT move: a word the vocabulary claims as a given name (``"abd née Jones"``) and a word written as an initial (``"J. née Jones Smith V"``) -- this rule changes what POSITION decided and does not reach what a word already is. **If you have code that reads the lone name word beside a maiden clause out of given, this is the release where it moves to family** (closes #445) - - Change what a star import of the two 2.2 vocabulary modules binds. ``nameparser.config.particles`` and ``nameparser.config.bound_given_names`` now declare ``__all__``, which their 1.x shims already did, so ``from ... import *`` binds their vocabulary alone -- it also bound the ``assert_normalized`` invariant helper, and from ``particles`` the ``BOUND_GIVEN_NAMES`` it imports only for a disjointness check. Importing a constant by name is unaffected, and no parse changes (#356) + - **Change what a star import of the two 2.2 vocabulary modules binds.** ``nameparser.config.particles`` and ``nameparser.config.bound_given_names`` now declare ``__all__``, which their 1.x shims already did, so ``from ... import *`` binds their vocabulary alone -- it also bound the ``assert_normalized`` invariant helper, and from ``particles`` the ``BOUND_GIVEN_NAMES`` it imports only for a disjointness check. Importing a constant by name is unaffected, and no parse changes (#356) **Deprecations** @@ -142,42 +142,42 @@ Release Log **East Asian name support** - - Add the Chinese locale pack ``locales.ZH``: opt-in Han segmentation for unspaced names like ``毛泽东``, with the surname vocabulary it needs. A pack rather than a default because a Chinese surname list corrupts Japanese names written in the same characters (``高橋一郎`` would split ``高`` + ``橋一郎``). Japanese data goes through ``locales.JA`` instead. See :doc:`locales` (#271) - - Add ``Lexicon.surnames``, ``Policy.script_orders``, ``Policy.segment_scripts``, the ``Script`` enum and the ``DEFAULT_SCRIPT_ORDERS`` constant to the public API. This is the first behavior nameparser keys on the script a name is written in, allowed only where the script itself settles a convention and never as a proxy for guessing the language. See :doc:`modules` (#271) - - Add ``Lexicon.honorific_tails``, the vocabulary the glued-honorific peel matches: entries that may be split off the end of a name token. It is a narrower set than the spaced honorific vocabulary, since a glued tail has no token boundary to lean on, and every entry must also be a ``suffix_words`` entry. Extend both in one call, or adding to ``honorific_tails`` alone raises ``ValueError``. See :doc:`customize` (#308) - - Add ``AmbiguityKind.SEGMENTATION``, reported when a surname split had a vocabulary-supported alternative: ``"남궁민수"`` is 남궁 + 민수 by the compound surname but 남 + 궁민수 by the single-syllable one, and longest-match had to pick. A name with only one possible split reports nothing (#271) - - Add the Japanese locale pack ``locales.JA`` and the segmenter factory ``locales.ja_segmenter()``, which together divide an unspaced Japanese name: ``parser_for(locales.JA, segmenter=locales.ja_segmenter())`` reads ``山田太郎`` as family ``山田``, given ``太郎``. They are separate because no surname list can do this job, so the pack activates the stage and a third-party divider performs it. ``ja_segmenter()`` wraps `namedivider-python `_, installed with the new ``nameparser[ja]`` extra; the core stays dependency-free. ``locales.available()`` is now ``('ja', 'ru', 'tr_az', 'zh')``. See :doc:`locales` (closes #272) - - Add ``Segmentation`` and the ``Segmenter`` type alias to the public API, plus the keyword-only ``Parser(segmenter=...)`` hook: any callable from a token's text to a ``Segmentation`` or to ``None`` to decline. It is consulted only for scripts in ``Policy.segment_scripts``, and only where the surname vocabulary declined first. Note what that ordering means when packs are stacked: a Japanese name opening on a listed Chinese surname never reaches the segmenter, so ``高橋一郎`` still splits ``高`` + ``橋一郎``. The packs are alternatives, one per corpus. See :ref:`segmenter-contract` (#272) - - Add a construction-time ``UserWarning`` when a parser activates segmentation for scripts nothing can divide. ``parser_for(locales.JA)`` without ``segmenter=`` used to build a parser that behaved like a working one minus the feature, silently. It now names the dead scripts and the call to pass. Any configured segmenter or covering surname vocabulary silences it, so the default parser and the ``zh`` pack never warn. A from-scratch lexicon with no hangul surnames warns under the default policy, with ``Policy(segment_scripts=frozenset())`` offered as the deactivation. See :ref:`east-asian-names` - - Add the ``Script`` members ``HIRAGANA`` and ``KATAKANA``. Two members rather than one ``KANA`` because the parser treats them differently: hiragana never transcribes a foreign name, while a wholly-katakana name usually is one (#272) + - **Add the Chinese locale pack locales.ZH:** opt-in Han segmentation for unspaced names like ``毛泽东``, with the surname vocabulary it needs. A pack rather than a default because a Chinese surname list corrupts Japanese names written in the same characters (``高橋一郎`` would split ``高`` + ``橋一郎``). Japanese data goes through ``locales.JA`` instead. See :doc:`locales` (#271) + - **Add Lexicon.surnames, Policy.script_orders, Policy.segment_scripts, the Script enum and the DEFAULT_SCRIPT_ORDERS constant to the public API.** This is the first behavior nameparser keys on the script a name is written in, allowed only where the script itself settles a convention and never as a proxy for guessing the language. See :doc:`modules` (#271) + - **Add Lexicon.honorific_tails, the vocabulary the glued-honorific peel matches:** entries that may be split off the end of a name token. It is a narrower set than the spaced honorific vocabulary, since a glued tail has no token boundary to lean on, and every entry must also be a ``suffix_words`` entry. Extend both in one call, or adding to ``honorific_tails`` alone raises ``ValueError``. See :doc:`customize` (#308) + - **Add AmbiguityKind.SEGMENTATION, reported when a surname split had a vocabulary-supported alternative:** ``"남궁민수"`` is 남궁 + 민수 by the compound surname but 남 + 궁민수 by the single-syllable one, and longest-match had to pick. A name with only one possible split reports nothing (#271) + - **Add the Japanese locale pack locales.JA and the segmenter factory locales.ja_segmenter(), which together divide an unspaced Japanese name:** ``parser_for(locales.JA, segmenter=locales.ja_segmenter())`` reads ``山田太郎`` as family ``山田``, given ``太郎``. They are separate because no surname list can do this job, so the pack activates the stage and a third-party divider performs it. ``ja_segmenter()`` wraps `namedivider-python `_, installed with the new ``nameparser[ja]`` extra; the core stays dependency-free. ``locales.available()`` is now ``('ja', 'ru', 'tr_az', 'zh')``. See :doc:`locales` (closes #272) + - **Add Segmentation and the Segmenter type alias to the public API, plus the keyword-only Parser(segmenter=...) hook:** any callable from a token's text to a ``Segmentation`` or to ``None`` to decline. It is consulted only for scripts in ``Policy.segment_scripts``, and only where the surname vocabulary declined first. Note what that ordering means when packs are stacked: a Japanese name opening on a listed Chinese surname never reaches the segmenter, so ``高橋一郎`` still splits ``高`` + ``橋一郎``. The packs are alternatives, one per corpus. See :ref:`segmenter-contract` (#272) + - **Add a construction-time UserWarning when a parser activates segmentation for scripts nothing can divide.** ``parser_for(locales.JA)`` without ``segmenter=`` used to build a parser that behaved like a working one minus the feature, silently. It now names the dead scripts and the call to pass. Any configured segmenter or covering surname vocabulary silences it, so the default parser and the ``zh`` pack never warn. A from-scratch lexicon with no hangul surnames warns under the default policy, with ``Policy(segment_scripts=frozenset())`` offered as the deactivation. See :ref:`east-asian-names` + - **Add the Script members HIRAGANA and KATAKANA.** Two members rather than one ``KANA`` because the parser treats them differently: hiragana never transcribes a foreign name, while a wholly-katakana name usually is one (#272) **Breaking Changes** - - Change the pickle compatibility of ``Policy`` and ``Lexicon``: the new fields change the field layout their guarded ``__setstate__`` checks, so a pickle written by 2.0.0 raises ``ValueError`` naming the missing fields instead of loading. Re-pickle after upgrading. ``HumanName`` pickles are unaffected, since the facade pickles v1-shaped component state rather than these objects - - Change the pickle compatibility of ``Parser`` the same way, for the new ``segmenter`` field. Re-pickle after upgrading. A ``Parser`` carrying a segmenter pickles only if that segmenter does, which a module-level function does and a closure or lambda does not (#272) - - Change one thing about parse totality: ``parse()`` still never raises on any input, but a user-supplied ``Parser(segmenter=...)`` runs inside the parse and its own exceptions propagate rather than being absorbed. A failure there is a bug in your callable, not a fact about the name (#272) + - **Change the pickle compatibility of Policy and Lexicon:** the new fields change the field layout their guarded ``__setstate__`` checks, so a pickle written by 2.0.0 raises ``ValueError`` naming the missing fields instead of loading. Re-pickle after upgrading. ``HumanName`` pickles are unaffected, since the facade pickles v1-shaped component state rather than these objects + - **Change the pickle compatibility of Parser the same way, for the new segmenter field.** Re-pickle after upgrading. A ``Parser`` carrying a segmenter pickles only if that segmenter does, which a module-level function does and a closure or lambda does not (#272) + - **Change one thing about parse totality:** ``parse()`` still never raises on any input, but a user-supplied ``Parser(segmenter=...)`` runs inside the parse and its own exceptions propagate rather than being absorbed. A failure there is a bug in your callable, not a fact about the name (#272) **Behavior Changes** - - Fix names written wholly in Han or hangul parsing given-first. Native-script CJK now reads family-first through the new ``Policy.script_orders`` table, so ``"毛 泽东"`` gives family ``毛`` where 1.x gave family ``泽东``. A single unspaced token moves the same way, which is the easiest form of this to miss: ``"毛泽东"`` renders identically whichever field holds it, while the spaced form shows the change (``str(HumanName("毛 泽东"))`` is now ``"泽东 毛"``). No language detection is involved, and an explicit comma still wins. **Default-on**, and it reaches ``HumanName``. See :ref:`east-asian-names` (closes #271) - - Fix unspaced Korean names not splitting. The census surname list now ships as default vocabulary with hangul segmentation on by default, so ``"김민준"`` parses family ``김``, given ``민준`` where 1.x returned the whole string as ``first``. Rendering follows the split. Nothing but Korean is written in hangul and its surnames are a closed set, which is what makes this safe as a default rather than a pack. **Default-on**. See :doc:`customize` for the two switches (#271) - - Fix Japanese names carrying kana parsing given-first. The family-first rule extends to any name whose characters stay within kanji and kana while carrying at least one kana character, so ``"高橋 みなみ"`` gives family ``高橋``. The reasoning is the one hangul already uses: hiragana never transcribes a foreign name, and a transcription is kana alone, so kanji-plus-kana is a Japanese name in Japanese order. A name written **wholly in katakana** is excluded and stays positional, being predominantly a transcribed foreign name. **Default-on**. See :ref:`east-asian-names` (#272) - - Fix names containing 〆 (U+3006, the shime mark opening Japanese surnames like 〆木) parsing given-first. The script classifier now counts it as Han, so these names take the family-first reading like any other wholly-Han name (#303) - - Fix the katakana middle dot ``・`` (U+30FB, and its halfwidth twin U+FF65) being read as part of a name rather than as a divider. It now separates tokens exactly as a space does, so ``"マイケル・ジャクソン"`` gives given ``マイケル``, family ``ジャクソン`` where 1.x left the whole string in ``first``. Native Japanese names never contain this character, so the separation is unconditional and the policy opt-outs do **not** cover it. Rendering returns the dot as a space (#272) - - Fix 间隔号-divided transcriptions parsing as one unsplit token. U+00B7, the interpunct Chinese text divides a transcribed foreign name with, is now a token separator between characters of a classified script, and a name it divides keeps its source order and is never segmented. It divides only between classified-script characters, so the Catalan punt volat in ``Gal·la`` is untouched. The Japanese nakaguro is deliberately not a transcription marker, so ``高橋・一郎`` keeps its family-first reading (#298) - - Fix spaced CJK postnominal honorifics parsing as name parts. 씨, 박사, 선생님, 교수님, 군, 양, 先生, 女士, 小姐, 博士, 教授, 様 and 氏 now route to ``suffix``, so ``王小明 先生`` reads family ``王小明`` where the family-first default had made 先生 the given name. See :ref:`east-asian-names` (closes #307) - - Fix glued CJK honorifics parsing as part of the name. ``田中さん``, ``김민준씨`` and ``王小明先生`` now split the honorific off the end of the name into ``suffix``. Previously it stayed in the name: ``田中さん`` was entirely the family name, and ``김민준씨`` gave given 민준씨. The peel runs before the name is split or ordered, so ``김민준씨`` still divides into family 김, given 민준. Only entries that can never end a name peel; 양, 군, 氏, 博士 and 殿 are recognized in their spaced form only, since 김지양 is a given name and some ninety Japanese surnames end in 殿. **Default-on**, and a lone family name written with a glued honorific now divides where it did not. See :ref:`east-asian-names` for the full set and :doc:`customize` for the off-switch (closes #308) - - Fix a comma or a 间隔号 stopping the glued-honorific peel. ``김, 민준씨`` now gives family 김, given 민준, suffix 씨, the same as the spaced ``김 민준씨``, and likewise ``田中, 太郎さん``. Previously each left the honorific inside the name. Both marks say where a name divides into surname and given, and an honorific is not part of the name in either reading. What a comma does instead is say which runs to look in: the two around a family comma. Anything past those is out of reach, so ``김, 민준 지훈씨`` peels while ``김, 민준, 지훈씨`` does not. **Default-on**. See :ref:`east-asian-names` (closes #312) - - Fix a glued honorific staying inside the name when the whole post-comma remainder is a credential. ``田中さん, V.`` and ``田中さん, Ph. D.`` now give up さん to ``suffix`` the way ``田中さん, PhD`` already did. The peel had been taking the post-comma run for name text on the strength of the comma alone, walking into the credentials and abandoning the peel there. That run is now tested with the same rule that decides the comma structure, and declined where it is credentials, provided the part before the comma offers a peel site of its own. Where the credential itself lands is still the comma's business and still differs by spelling. **Default-on** (closes #319) - - Fix an ASCII period after a CJK honorific stopping it being recognized. ``씨.``, ``様.``, ``氏.``, ``님.``, ``군.``, ``양.`` and ``殿.`` now route to ``suffix`` like their periodless spellings, where the trailing period had left them inside the name. The cause was v1's initial regex, whose ``\w`` is Unicode-aware and matched a hangul syllable or Han ideograph as readily as a letter; a veto written for Latin was being asked of scripts it was never about. Alphabets keep their initials untouched, and ``"А. С. Пушкин"`` is unaffected. Read *period* strictly: only the ASCII full stop is covered, so ``"김민준 씨."`` written with the fullwidth stop still reads the honorific as the family name. **Default-on** (#320) - - Fix NFD-decomposed input missing the East Asian defaults entirely. Script classification now normalizes to NFC before deciding, so a Korean or Japanese name typed on macOS, where decomposed text is routine, gets the same order rule as its composed twin. Segmentation matching deliberately stays raw, so an unspaced NFD hangul name is ordered correctly but not split, rather than being split in the wrong place. One gotcha: parse output preserves the encoding it was given, so for NFD input ``name.family == "김"`` is ``False`` even though it is the same name. Compare NFC-normalized text when comparing across encodings. See :ref:`east-asian-names` (#272) - - Fix the Ukrainian conjunction ``й`` not joining the pieces around it. It is the euphonic alternate of ``і``, chosen by the surrounding sounds rather than by meaning, so real Ukrainian data carries both spellings. ``"Олесь й Олена Коваленки"`` now gives given ``"Олесь й Олена"`` where the ``й`` previously landed in ``middle``. Same treatment as the ``и``/``і`` entries added in 2.0.0: the conjunction joins only once the name has enough pieces, and a punctuated initial still wins, so ``"Й. Сліпий"`` is unaffected. Raised in a comment on #267 - - Add the Japanese maiden-name marker ``旧姓`` to the default vocabulary. ``"山田花子 旧姓 佐藤"`` now gives family ``山田花子`` and maiden ``佐藤``, where 1.4.0 left the marker in the name. It sits beside the Cyrillic ``урожд.`` and German ``geb.`` entries rather than in ``locales.JA``, on the rule that admitted those: a native-script marker cannot collide with a Latin-script name, so it is safe as a default. Matching is whole-token, so the marker has to *be* a token, which for Japanese means a space or a configured delimiter must divide it from the name. The fullwidth colon does not, so ``"山田(旧姓:佐藤)"`` still returns maiden ``"旧姓:佐藤"``; that one wants the head-peel #317 tracks. **Default-on**. See :doc:`customize` (#309) - - Fix a maiden marker inside bracketed content staying in the ``maiden`` value. Where a delimiter pair is routed to ``maiden`` by ``Policy(maiden_delimiters=...)``, a marker at the head of the clause is now dropped the way it always has been in the bare form, so ``"Jane Smith (née Jones)"`` gives maiden ``Jones``, the same answer as the unbracketed spelling. Each clause loses its own leading marker, and only where the clause holds more than one token, so ``"Jane Smith (Nee) (Jones)"`` still gives maiden ``Nee Jones`` (``Nee`` is a real surname). Scope before you count on it: ``Policy.maiden_delimiters`` is **empty by default**, so under the default policy brackets route to ``nickname`` and none of this applies. See :doc:`customize` (closes #329) + - **Fix names written wholly in Han or hangul parsing given-first.** Native-script CJK now reads family-first through the new ``Policy.script_orders`` table, so ``"毛 泽东"`` gives family ``毛`` where 1.x gave family ``泽东``. A single unspaced token moves the same way, which is the easiest form of this to miss: ``"毛泽东"`` renders identically whichever field holds it, while the spaced form shows the change (``str(HumanName("毛 泽东"))`` is now ``"泽东 毛"``). No language detection is involved, and an explicit comma still wins. **Default-on**, and it reaches ``HumanName``. See :ref:`east-asian-names` (closes #271) + - **Fix unspaced Korean names not splitting.** The census surname list now ships as default vocabulary with hangul segmentation on by default, so ``"김민준"`` parses family ``김``, given ``민준`` where 1.x returned the whole string as ``first``. Rendering follows the split. Nothing but Korean is written in hangul and its surnames are a closed set, which is what makes this safe as a default rather than a pack. **Default-on**. See :doc:`customize` for the two switches (#271) + - **Fix Japanese names carrying kana parsing given-first.** The family-first rule extends to any name whose characters stay within kanji and kana while carrying at least one kana character, so ``"高橋 みなみ"`` gives family ``高橋``. The reasoning is the one hangul already uses: hiragana never transcribes a foreign name, and a transcription is kana alone, so kanji-plus-kana is a Japanese name in Japanese order. A name written **wholly in katakana** is excluded and stays positional, being predominantly a transcribed foreign name. **Default-on**. See :ref:`east-asian-names` (#272) + - **Fix names containing 〆 (U+3006, the shime mark opening Japanese surnames like 〆木) parsing given-first.** The script classifier now counts it as Han, so these names take the family-first reading like any other wholly-Han name (#303) + - **Fix the katakana middle dot ・ (U+30FB, and its halfwidth twin U+FF65) being read as part of a name rather than as a divider.** It now separates tokens exactly as a space does, so ``"マイケル・ジャクソン"`` gives given ``マイケル``, family ``ジャクソン`` where 1.x left the whole string in ``first``. Native Japanese names never contain this character, so the separation is unconditional and the policy opt-outs do **not** cover it. Rendering returns the dot as a space (#272) + - **Fix 间隔号-divided transcriptions parsing as one unsplit token.** U+00B7, the interpunct Chinese text divides a transcribed foreign name with, is now a token separator between characters of a classified script, and a name it divides keeps its source order and is never segmented. It divides only between classified-script characters, so the Catalan punt volat in ``Gal·la`` is untouched. The Japanese nakaguro is deliberately not a transcription marker, so ``高橋・一郎`` keeps its family-first reading (#298) + - **Fix spaced CJK postnominal honorifics parsing as name parts.** 씨, 박사, 선생님, 교수님, 군, 양, 先生, 女士, 小姐, 博士, 教授, 様 and 氏 now route to ``suffix``, so ``王小明 先生`` reads family ``王小明`` where the family-first default had made 先生 the given name. See :ref:`east-asian-names` (closes #307) + - **Fix glued CJK honorifics parsing as part of the name.** ``田中さん``, ``김민준씨`` and ``王小明先生`` now split the honorific off the end of the name into ``suffix``. Previously it stayed in the name: ``田中さん`` was entirely the family name, and ``김민준씨`` gave given 민준씨. The peel runs before the name is split or ordered, so ``김민준씨`` still divides into family 김, given 민준. Only entries that can never end a name peel; 양, 군, 氏, 博士 and 殿 are recognized in their spaced form only, since 김지양 is a given name and some ninety Japanese surnames end in 殿. **Default-on**, and a lone family name written with a glued honorific now divides where it did not. See :ref:`east-asian-names` for the full set and :doc:`customize` for the off-switch (closes #308) + - **Fix a comma or a 间隔号 stopping the glued-honorific peel.** ``김, 민준씨`` now gives family 김, given 민준, suffix 씨, the same as the spaced ``김 민준씨``, and likewise ``田中, 太郎さん``. Previously each left the honorific inside the name. Both marks say where a name divides into surname and given, and an honorific is not part of the name in either reading. What a comma does instead is say which runs to look in: the two around a family comma. Anything past those is out of reach, so ``김, 민준 지훈씨`` peels while ``김, 민준, 지훈씨`` does not. **Default-on**. See :ref:`east-asian-names` (closes #312) + - **Fix a glued honorific staying inside the name when the whole post-comma remainder is a credential.** ``田中さん, V.`` and ``田中さん, Ph. D.`` now give up さん to ``suffix`` the way ``田中さん, PhD`` already did. The peel had been taking the post-comma run for name text on the strength of the comma alone, walking into the credentials and abandoning the peel there. That run is now tested with the same rule that decides the comma structure, and declined where it is credentials, provided the part before the comma offers a peel site of its own. Where the credential itself lands is still the comma's business and still differs by spelling. **Default-on** (closes #319) + - **Fix an ASCII period after a CJK honorific stopping it being recognized.** ``씨.``, ``様.``, ``氏.``, ``님.``, ``군.``, ``양.`` and ``殿.`` now route to ``suffix`` like their periodless spellings, where the trailing period had left them inside the name. The cause was v1's initial regex, whose ``\w`` is Unicode-aware and matched a hangul syllable or Han ideograph as readily as a letter; a veto written for Latin was being asked of scripts it was never about. Alphabets keep their initials untouched, and ``"А. С. Пушкин"`` is unaffected. Read *period* strictly: only the ASCII full stop is covered, so ``"김민준 씨."`` written with the fullwidth stop still reads the honorific as the family name. **Default-on** (#320) + - **Fix NFD-decomposed input missing the East Asian defaults entirely.** Script classification now normalizes to NFC before deciding, so a Korean or Japanese name typed on macOS, where decomposed text is routine, gets the same order rule as its composed twin. Segmentation matching deliberately stays raw, so an unspaced NFD hangul name is ordered correctly but not split, rather than being split in the wrong place. One gotcha: parse output preserves the encoding it was given, so for NFD input ``name.family == "김"`` is ``False`` even though it is the same name. Compare NFC-normalized text when comparing across encodings. See :ref:`east-asian-names` (#272) + - **Fix the Ukrainian conjunction й not joining the pieces around it.** It is the euphonic alternate of ``і``, chosen by the surrounding sounds rather than by meaning, so real Ukrainian data carries both spellings. ``"Олесь й Олена Коваленки"`` now gives given ``"Олесь й Олена"`` where the ``й`` previously landed in ``middle``. Same treatment as the ``и``/``і`` entries added in 2.0.0: the conjunction joins only once the name has enough pieces, and a punctuated initial still wins, so ``"Й. Сліпий"`` is unaffected. Raised in a comment on #267 + - **Add the Japanese maiden-name marker 旧姓 to the default vocabulary.** ``"山田花子 旧姓 佐藤"`` now gives family ``山田花子`` and maiden ``佐藤``, where 1.4.0 left the marker in the name. It sits beside the Cyrillic ``урожд.`` and German ``geb.`` entries rather than in ``locales.JA``, on the rule that admitted those: a native-script marker cannot collide with a Latin-script name, so it is safe as a default. Matching is whole-token, so the marker has to *be* a token, which for Japanese means a space or a configured delimiter must divide it from the name. The fullwidth colon does not, so ``"山田(旧姓:佐藤)"`` still returns maiden ``"旧姓:佐藤"``; that one wants the head-peel #317 tracks. **Default-on**. See :doc:`customize` (#309) + - **Fix a maiden marker inside bracketed content staying in the maiden value.** Where a delimiter pair is routed to ``maiden`` by ``Policy(maiden_delimiters=...)``, a marker at the head of the clause is now dropped the way it always has been in the bare form, so ``"Jane Smith (née Jones)"`` gives maiden ``Jones``, the same answer as the unbracketed spelling. Each clause loses its own leading marker, and only where the clause holds more than one token, so ``"Jane Smith (Nee) (Jones)"`` still gives maiden ``Nee Jones`` (``Nee`` is a real surname). Scope before you count on it: ``Policy.maiden_delimiters`` is **empty by default**, so under the default policy brackets route to ``nickname`` and none of this applies. See :doc:`customize` (closes #329) **Documentation** - - Correct the documented scope of the period-abbreviation title rule. It was described as applying to "a leading word", which was never true of any comma form: the rule runs at the front of the part that carries the given name, which after a family comma is the part *after* the comma, so ``"Morse, Det. Insp. Jane"`` gives title ``Det. Insp.``. Behavior is unchanged and matches 1.4.0; only the description was wrong + - **Correct the documented scope of the period-abbreviation title rule.** It was described as applying to "a leading word", which was never true of any comma form: the rule runs at the front of the part that carries the given name, which after a family comma is the part *after* the comma, so ``"Morse, Det. Insp. Jane"`` gives title ``Det. Insp.``. Behavior is unchanged and matches 1.4.0; only the description was wrong * 2.0.0 - July 27, 2026 @@ -201,35 +201,35 @@ Release Log **The 2.0 API** - - Add ``parse(text)``, returning an immutable ``ParsedName`` with seven fields -- ``title``, ``given``, ``middle``, ``family``, ``suffix``, ``nickname``, ``maiden`` -- plus the derived views ``given_names``, ``surnames``, ``family_base`` and ``family_particles``. Parsing is a pure function of the text, a ``Lexicon`` and a ``Policy``: nothing global is consulted and nothing is mutated - - Add ``Lexicon``, the frozen vocabulary object. ``Lexicon.default()`` is the shipped vocabulary and ``Lexicon.empty()`` is a blank one; ``add(**entries)``, ``remove(**entries)`` and field-wise ``|`` all return new instances. Its fields are ``titles``, ``given_name_titles``, ``suffix_acronyms``, ``suffix_words``, ``suffix_acronyms_ambiguous``, ``particles``, ``particles_ambiguous``, ``conjunctions``, ``bound_given_names``, ``maiden_markers`` and ``capitalization_exceptions`` + - **Add parse(text), returning an immutable ParsedName with seven fields** -- ``title``, ``given``, ``middle``, ``family``, ``suffix``, ``nickname``, ``maiden`` -- plus the derived views ``given_names``, ``surnames``, ``family_base`` and ``family_particles``. Parsing is a pure function of the text, a ``Lexicon`` and a ``Policy``: nothing global is consulted and nothing is mutated + - **Add Lexicon, the frozen vocabulary object.** ``Lexicon.default()`` is the shipped vocabulary and ``Lexicon.empty()`` is a blank one; ``add(**entries)``, ``remove(**entries)`` and field-wise ``|`` all return new instances. Its fields are ``titles``, ``given_name_titles``, ``suffix_acronyms``, ``suffix_words``, ``suffix_acronyms_ambiguous``, ``particles``, ``particles_ambiguous``, ``conjunctions``, ``bound_given_names``, ``maiden_markers`` and ``capitalization_exceptions`` - Add ``Policy``, the frozen behavior object: ``name_order``, ``patronymic_rules``, ``middle_as_family``, ``nickname_delimiters``, ``maiden_delimiters``, ``extra_suffix_delimiters``, ``lenient_comma_suffixes``, ``strip_emoji`` and ``strip_bidi`` - Add ``Parser``, a reusable parser bound to a lexicon and policy (``Parser(lexicon=..., policy=...).parse(text)``), and ``parser_for(*locales, base=None)``, which folds locale packs onto a base parser - Add ``name_order`` and the constants ``GIVEN_FIRST``, ``FAMILY_FIRST`` and ``FAMILY_FIRST_GIVEN_LAST``, so a family-first name can be parsed as written rather than reordered by hand (the configuration half of #270; the locale packs below complete it) - Add ``PatronymicRule`` with the members ``EAST_SLAVIC`` and ``TURKIC``. v1's single ``patronymic_name_order`` flag enabled both detectors at once; ``Policy(patronymic_rules=...)`` lets you enable either one alone - - Add ``PolicyPatch`` and the ``UNSET`` sentinel for partial policy deltas that compose -- set-valued fields union, scalar fields override with later winning. This is the mechanism locale packs are built from, and ``UNSET`` is only needed when you must distinguish "not set" from a real ``False`` or ``None`` - - Add ``Token``, ``Span`` and ``Role``: every field is backed by tokens carrying exact ``(start, end)`` offsets into the original string, reachable with ``tokens_for(Role.GIVEN)``. This replaces v1's ``*_list`` attributes and makes it possible to highlight or re-slice the input the parse came from. ``Role`` is a ``StrEnum``, so members compare and stringify as their field names (``Role.GIVEN == "given"``), matching ``AmbiguityKind``; ``tokens_for()`` accepts a role's string name too, and raises ``ValueError`` for an unknown role + - **Add PolicyPatch and the UNSET sentinel for partial policy deltas that compose** -- set-valued fields union, scalar fields override with later winning. This is the mechanism locale packs are built from, and ``UNSET`` is only needed when you must distinguish "not set" from a real ``False`` or ``None`` + - **Add Token, Span and Role:** every field is backed by tokens carrying exact ``(start, end)`` offsets into the original string, reachable with ``tokens_for(Role.GIVEN)``. This replaces v1's ``*_list`` attributes and makes it possible to highlight or re-slice the input the parse came from. ``Role`` is a ``StrEnum``, so members compare and stringify as their field names (``Role.GIVEN == "given"``), matching ``AmbiguityKind``; ``tokens_for()`` accepts a role's string name too, and raises ``ValueError`` for an unknown role - Add ``STABLE_TAGS`` to the public API: the four documented ``Token.tags`` values (``particle``, ``conjunction``, ``initial``, ``joined``) - Add ``Policy.patched(patch)``, applying a ``PolicyPatch`` directly without wrapping it in a locale pack - - Add ``Parser.matches(a, b)`` and ``Parser.capitalized(name)``: the ``ParsedName`` methods of the same names fall back to the default configuration for str/omitted arguments, which is silently wrong for names parsed with a custom ``Parser`` + - **Add Parser.matches(a, b) and Parser.capitalized(name):** the ``ParsedName`` methods of the same names fall back to the default configuration for str/omitted arguments, which is silently wrong for names parsed with a custom ``Parser`` - Add ``Parser.revise(name, **fields)``: ``ParsedName.replace()`` with the replacement text classified by the parser's vocabulary, so particle/initial/suffix-join behavior survives the edit - - Add ``Ambiguity`` and the ``AmbiguityKind`` enum, so a parse reports what it had to guess at instead of guessing silently. The kinds emitted today are ``particle-or-given``, ``suffix-or-name``, ``suffix-or-nickname``, ``unbalanced-delimiter`` and ``comma-structure``; ``order`` is reserved and not yet emitted. The two suffix kinds cover the post-nominals that are also ordinary words: ``"John Smith MA"`` reports that ``MA`` was read as a credential rather than a surname, and ``"JEFFREY (JD) BRICKEN"`` that the delimited ``JD`` was read as a nickname rather than a suffix. A reading the vocabulary settles on its own — ``"John Smith M.A."``, ``"Andrew Perkins (MBA)"`` — is not a guess and reports nothing - - Add ``ParsedName`` output and comparison methods: ``render(spec)``, ``initials()``, ``capitalized()`` (which returns a new value rather than mutating in place), ``as_dict()`` (whose ``include_empty`` flag is keyword-only, unlike ``HumanName.as_dict()``'s), ``replace(**fields)``, ``matches()`` and ``comparison_key()`` + - **Add Ambiguity and the AmbiguityKind enum, so a parse reports what it had to guess at instead of guessing silently.** The kinds emitted today are ``particle-or-given``, ``suffix-or-name``, ``suffix-or-nickname``, ``unbalanced-delimiter`` and ``comma-structure``; ``order`` is reserved and not yet emitted. The two suffix kinds cover the post-nominals that are also ordinary words: ``"John Smith MA"`` reports that ``MA`` was read as a credential rather than a surname, and ``"JEFFREY (JD) BRICKEN"`` that the delimited ``JD`` was read as a nickname rather than a suffix. A reading the vocabulary settles on its own — ``"John Smith M.A."``, ``"Andrew Perkins (MBA)"`` — is not a guess and reports nothing + - **Add ParsedName output and comparison methods:** ``render(spec)``, ``initials()``, ``capitalized()`` (which returns a new value rather than mutating in place), ``as_dict()`` (whose ``include_empty`` flag is keyword-only, unlike ``HumanName.as_dict()``'s), ``replace(**fields)``, ``matches()`` and ``comparison_key()`` - Add ``Locale``, the public pack type. Writing your own needs no registration -- construct a ``Locale`` and pass it to ``parser_for()`` - Ship a fully typed public API (PEP 561): the core modules are checked under strict mypy settings, and nameparser 2.0 has no runtime dependencies **Breaking Changes** - Raise the minimum Python to 3.11 and drop the last runtime dependency, ``typing_extensions`` (#257). Python 3.10 is no longer supported - - Remove ``HumanName.__eq__`` and ``__hash__`` (deprecated in 1.3.0, #223): instances now compare and hash by identity, so ``HumanName("John Smith") == "John Smith"`` is ``False`` where 1.x returned ``True``. **This changes result silently rather than raising** -- it is the one removal that can pass unnoticed into production. Use ``matches()`` to ask whether two names are the same, and ``comparison_key()`` as a dict key or sort key - - Remove every v1 parsing hook and subclass extension point: ``pre_process``, ``post_process``, ``parse_pieces``, ``parse_nicknames``, ``join_on_conjunctions``, the ``is_*`` predicates, ``cap_word``/``cap_piece``, ``handle_firstnames``, ``fix_phd`` and the rest. A subclass that overrides one gets a ``DeprecationWarning`` at construction naming the hooks, because the facade delegates to the core ``Parser`` and never calls them (closes #280). Customize through ``Lexicon``/``Policy`` instead - - Remove regex configuration: ``CONSTANTS.regexes`` is now a read-only proxy. Reads still work, but ``CONSTANTS.regexes.bidi = False``, item assignment and ``Constants(regexes=...)`` all raise ``TypeError``. **If you followed 1.3.1's advice to keep bidi marks with** ``CONSTANTS.regexes.bidi = False``, that opt-out is now ``Policy(strip_bidi=False)`` on the 2.0 API; the same applies to ``regexes.emoji`` and ``Policy(strip_emoji=False)`` - - Remove ``bytes`` input and the ``encoding`` argument (#245): passing ``bytes`` to ``HumanName`` or to a set manager raises ``TypeError`` with a decode hint, and ``SetManager.add_with_encoding()`` and ``DEFAULT_ENCODING`` are gone. Decode first, then use ``add()`` - - Remove ``SetManager.__call__`` (#243); iterate the manager or call ``set(manager)``. ``remove()`` of a missing member now raises ``KeyError`` like ``set.remove`` -- ``discard()`` is the ignore-missing form -- and the set operators ``|``, ``&``, ``-`` and ``^`` return a plain ``set`` - - Remove ``HumanName`` slice access and item assignment (#258): ``name[1:-3]`` and ``name['first'] = value`` raise ``TypeError``. String-key reads (``name['first']``) and iteration are unchanged; assign fields as plain attributes + - **Remove HumanName.__eq__ and __hash__ (deprecated in 1.3.0, #223):** instances now compare and hash by identity, so ``HumanName("John Smith") == "John Smith"`` is ``False`` where 1.x returned ``True``. **This changes result silently rather than raising** -- it is the one removal that can pass unnoticed into production. Use ``matches()`` to ask whether two names are the same, and ``comparison_key()`` as a dict key or sort key + - **Remove every v1 parsing hook and subclass extension point:** ``pre_process``, ``post_process``, ``parse_pieces``, ``parse_nicknames``, ``join_on_conjunctions``, the ``is_*`` predicates, ``cap_word``/``cap_piece``, ``handle_firstnames``, ``fix_phd`` and the rest. A subclass that overrides one gets a ``DeprecationWarning`` at construction naming the hooks, because the facade delegates to the core ``Parser`` and never calls them (closes #280). Customize through ``Lexicon``/``Policy`` instead + - **Remove regex configuration:** ``CONSTANTS.regexes`` is now a read-only proxy. Reads still work, but ``CONSTANTS.regexes.bidi = False``, item assignment and ``Constants(regexes=...)`` all raise ``TypeError``. **If you followed 1.3.1's advice to keep bidi marks with** ``CONSTANTS.regexes.bidi = False``, that opt-out is now ``Policy(strip_bidi=False)`` on the 2.0 API; the same applies to ``regexes.emoji`` and ``Policy(strip_emoji=False)`` + - **Remove bytes input and the encoding argument (#245):** passing ``bytes`` to ``HumanName`` or to a set manager raises ``TypeError`` with a decode hint, and ``SetManager.add_with_encoding()`` and ``DEFAULT_ENCODING`` are gone. Decode first, then use ``add()`` + - **Remove SetManager.__call__ (#243); iterate the manager or call set(manager).** ``remove()`` of a missing member now raises ``KeyError`` like ``set.remove`` -- ``discard()`` is the ignore-missing form -- and the set operators ``|``, ``&``, ``-`` and ``^`` return a plain ``set`` + - **Remove HumanName slice access and item assignment (#258):** ``name[1:-3]`` and ``name['first'] = value`` raise ``TypeError``. String-key reads (``name['first']``) and iteration are unchanged; assign fields as plain attributes - Remove ``Constants.empty_attribute_default`` (#255): empty fields are always ``''``. Assigning it raises ``AttributeError``; a pickle carrying the key still loads, with the value ignored - Remove ``constants=None`` (#261): both ``HumanName(..., constants=None)`` and ``hn.C = None`` raise ``TypeError``. Use ``Constants()`` for library defaults or ``CONSTANTS.copy()`` for a private snapshot - - Remove silent unknown-key access on the mapping managers (#256): ``CONSTANTS.capitalization_exceptions.typo`` and ``CONSTANTS.regexes.typo`` raise ``AttributeError`` naming the miss, instead of returning ``None``/``EMPTY_REGEX`` (``capitalization_exceptions`` also lists the known keys). ``.get()`` remains available on both for intentional soft access + - **Remove silent unknown-key access on the mapping managers (#256):** ``CONSTANTS.capitalization_exceptions.typo`` and ``CONSTANTS.regexes.typo`` raise ``AttributeError`` naming the miss, instead of returning ``None``/``EMPTY_REGEX`` (``capitalization_exceptions`` also lists the known keys). ``.get()`` remains available on both for intentional soft access - Remove support for ``Constants`` pickles written by nameparser 1.2.x or earlier (#279): loading one raises ``ValueError`` telling you to re-pickle under 1.3/1.4 first - Remove the dead ``regexes.no_vowels`` pattern (#268) and the ``Constants.suffixes_prefixes_titles`` cached union; neither was read by the parser - Change ``HumanName``'s ``*_list`` attributes to read-only properties: they remain readable snapshots, but ``name.first_list = [...]`` now raises ``AttributeError`` @@ -237,45 +237,45 @@ Release Log **Behavior Changes** - - Recognize maiden-name markers -- ``née``, ``nee``, ``geb.``, ``roz.`` and the Scandinavian participle forms -- and route the following name to the new ``maiden`` field. 1.x folded them into ``middle``/``last``, so ``"Jane Smith née Jones"`` parsed as ``middle="Smith née"`` (closes #274) - - Recognize typographic nickname delimiters by default in both APIs (closes #273): smart quotes (``“Jack”``), German and Polish low-high quotes (``„Hansi“``), Swedish right-right quotes (``”Ann”``), guillemets in either direction (``«Petit»``, ``»Hansi«``), CJK corner brackets (``「タロ」``, ``『ハナ』``) and fullwidth parentheses. In 1.x these leaked into ``middle`` as literal text. Curly *single* quotes stay excluded, because U+2019 is the apostrophe in "O’Connor" - - Fix the pre-comma piece being routed to ``first`` when everything after the comma is a suffix or title: ``"Andrews, M.D."`` now reads family ``Andrews`` / suffix ``M.D.`` where 1.x read given ``M.D.`` / family ``Andrews``, and ``"Smith, Dr."`` moves ``Smith`` from ``first`` to ``family`` (the title was already correct in 1.x). The piece before a comma is definitionally the family name - - Fix a lone recognized trailing suffix with no comma being routed to ``first``/``last``: ``"Johnson PhD"`` and ``"Mr. Johnson PhD"`` now keep the suffix in ``suffix`` - - Fix a split ``"Ph. D."`` credential being read as two tokens; it now classifies as one suffix, replacing v1's ``fix_phd`` hook. This now holds wherever the credential sits: 1.x healed the pair only when it trailed, so ``"Ph. D. John Smith"`` parsed as title ``Ph.`` / given ``D.`` with the real given name pushed to ``middle``; it now reads given ``John``, family ``Smith``, suffix ``Ph. D.`` - - Fix ``chargé d'affaires``: shipped as one unmatchable ``TITLES`` entry since it was added, it is now two chainable entries (``chargé``, ``d'affaires``), so the title is recognized; the unaccented spelling ``charge`` ships too, like ``attaché``/``attache`` + - **Recognize maiden-name markers** -- ``née``, ``nee``, ``geb.``, ``roz.`` and the Scandinavian participle forms -- and route the following name to the new ``maiden`` field. 1.x folded them into ``middle``/``last``, so ``"Jane Smith née Jones"`` parsed as ``middle="Smith née"`` (closes #274) + - **Recognize typographic nickname delimiters by default in both APIs (closes #273):** smart quotes (``“Jack”``), German and Polish low-high quotes (``„Hansi“``), Swedish right-right quotes (``”Ann”``), guillemets in either direction (``«Petit»``, ``»Hansi«``), CJK corner brackets (``「タロ」``, ``『ハナ』``) and fullwidth parentheses. In 1.x these leaked into ``middle`` as literal text. Curly *single* quotes stay excluded, because U+2019 is the apostrophe in "O’Connor" + - **Fix the pre-comma piece being routed to first when everything after the comma is a suffix or title:** ``"Andrews, M.D."`` now reads family ``Andrews`` / suffix ``M.D.`` where 1.x read given ``M.D.`` / family ``Andrews``, and ``"Smith, Dr."`` moves ``Smith`` from ``first`` to ``family`` (the title was already correct in 1.x). The piece before a comma is definitionally the family name + - **Fix a lone recognized trailing suffix with no comma being routed to first/last:** ``"Johnson PhD"`` and ``"Mr. Johnson PhD"`` now keep the suffix in ``suffix`` + - **Fix a split "Ph. D." credential being read as two tokens; it now classifies as one suffix, replacing v1's fix_phd hook.** This now holds wherever the credential sits: 1.x healed the pair only when it trailed, so ``"Ph. D. John Smith"`` parsed as title ``Ph.`` / given ``D.`` with the real given name pushed to ``middle``; it now reads given ``John``, family ``Smith``, suffix ``Ph. D.`` + - **Fix chargé d'affaires:** shipped as one unmatchable ``TITLES`` entry since it was added, it is now two chainable entries (``chargé``, ``d'affaires``), so the title is recognized; the unaccented spelling ``charge`` ships too, like ``attaché``/``attache`` - Remove seven multi-word ``SUFFIX_ACRONYMS`` entries (``leed ap``, ``nicet i``–``nicet iv``, ``psm i``, ``psm ii``) that could never match in any release; splitting them would swallow real names ("John Leed", "Smith, A.P."), so they are dropped instead - - Add a ``UserWarning`` when a multi-word entry is stored in a per-word ``Lexicon`` field or as a ``capitalization_exceptions`` key -- such entries can never match - - Parse an input with no alphanumeric character to an empty name in both APIs. 1.x kept pure punctuation as a name part, so ``"."`` gave ``first="."`` and ``bool()`` was ``True``; 2.0 empties it, keeping ``bool(parse(x))`` an honest "did I get a name?" test. The check is Unicode-aware, so names in any script are unaffected; only inputs that are entirely punctuation or symbols (``"."``, ``"- -"``) change. Junk embedded in a name with real content -- the stray dot in ``"John . Smith"`` -- is still kept, since that parse is already truthy - - Fold a leading never-given particle into the family name. Note that ``Lexicon.particles_ambiguous`` is the **complement** of v1's ``non_first_name_prefixes``, not a rename -- it lists the particles that *may* double as a given name, where v1 listed the ones that may not. Copying a v1 customization across without inverting it silently reverses the behavior; see :doc:`migrate` - - Add ``ma`` and ``do`` to ``suffix_acronyms_ambiguous``, the set of post-nominals that are also ordinary surnames. An entry there is read as a credential when the name can spare it — written with periods (``"John Smith M.A."``), or when removing it still leaves a given *and* a family name (``"John Smith MA"`` → suffix ``MA``). With only two pieces to go around, the surname reading wins instead: ``"Jack Ma"`` and ``"Anh Do"`` keep their family names. As a side effect, a parenthesized or quoted ``"(MA)"``/``"(DO)"`` now falls through to nickname parsing rather than escaping to ``suffix``, since inside delimiters the nickname reading is the plausible one - - Change ``comparison_key()`` and ``matches()`` in both APIs to fold with ``str.casefold()`` where 1.4 used ``str.lower()``, so Unicode case pairs compare equal -- ``"STRASSE"`` matches ``"Straße"``, and a Greek final sigma matches its regular form. This is strictly more permissive: anything 1.4 matched still matches. Vocabulary normalization deliberately still uses ``lower()``, for v1 parity - - Change the 2.0 API's default ``render()``/``str()`` spec to show every non-empty field: ``'{title} {given} "{nickname}" {middle} {family} ({maiden}) {suffix}'``. The quoted nickname round-trips exactly; the parenthesized maiden re-parses as a nickname, a deliberate choice of presentation over lossless round-trip -- use ``née {maiden}`` in a custom spec if you need it to survive a reparse. ``HumanName`` keeps v1's own ``string_format`` default, unchanged - - Change delimiter-overlap precedence in the 2.0 API: a pair listed in ``Policy.maiden_delimiters`` is dropped from the effective nickname set, so ``Policy(maiden_delimiters={("(", ")")})`` alone routes parenthesized content to ``maiden``. The default nickname set is exported as ``DEFAULT_NICKNAME_DELIMITERS``. ``HumanName`` keeps v1's nickname-wins precedence, so no existing behavior changes - - Change suffix-delimiter rendering when a custom suffix delimiter is configured: for ``suffix_delimiter="/"`` and ``"John Smith, RN/CRNA"``, 1.x split the token and rendered ``suffix="RN, CRNA"`` where 2.0 keeps it whole as ``"RN/CRNA"``. Role assignment is unchanged; only rendering differs - - Correct a long-standing typo in the shipped vocabulary that 1.x carried: ``actor`` and ``television`` had trailing spaces in ``TITLES``, so ``"actor" in TITLES`` was ``False``. **Parse output is unchanged** -- the parser normalizes entries on ingest, which is exactly what let the typo go unnoticed -- but code that tests membership against the exported constants directly will see corrected results. The data modules now assert their invariants at import time, alongside the ones ``prefixes.py`` already checked; entries must be stored lowercase and whitespace-free, so this class of typo now fails the build + - **Add a UserWarning when a multi-word entry is stored in a per-word Lexicon field or as a capitalization_exceptions key** -- such entries can never match + - **Parse an input with no alphanumeric character to an empty name in both APIs.** 1.x kept pure punctuation as a name part, so ``"."`` gave ``first="."`` and ``bool()`` was ``True``; 2.0 empties it, keeping ``bool(parse(x))`` an honest "did I get a name?" test. The check is Unicode-aware, so names in any script are unaffected; only inputs that are entirely punctuation or symbols (``"."``, ``"- -"``) change. Junk embedded in a name with real content -- the stray dot in ``"John . Smith"`` -- is still kept, since that parse is already truthy + - **Fold a leading never-given particle into the family name.** Note that ``Lexicon.particles_ambiguous`` is the **complement** of v1's ``non_first_name_prefixes``, not a rename -- it lists the particles that *may* double as a given name, where v1 listed the ones that may not. Copying a v1 customization across without inverting it silently reverses the behavior; see :doc:`migrate` + - **Add ma and do to suffix_acronyms_ambiguous, the set of post-nominals that are also ordinary surnames.** An entry there is read as a credential when the name can spare it — written with periods (``"John Smith M.A."``), or when removing it still leaves a given *and* a family name (``"John Smith MA"`` → suffix ``MA``). With only two pieces to go around, the surname reading wins instead: ``"Jack Ma"`` and ``"Anh Do"`` keep their family names. As a side effect, a parenthesized or quoted ``"(MA)"``/``"(DO)"`` now falls through to nickname parsing rather than escaping to ``suffix``, since inside delimiters the nickname reading is the plausible one + - **Change comparison_key() and matches() in both APIs to fold with str.casefold() where 1.4 used str.lower(), so Unicode case pairs compare equal** -- ``"STRASSE"`` matches ``"Straße"``, and a Greek final sigma matches its regular form. This is strictly more permissive: anything 1.4 matched still matches. Vocabulary normalization deliberately still uses ``lower()``, for v1 parity + - **Change the 2.0 API's default render()/str() spec to show every non-empty field:** ``'{title} {given} "{nickname}" {middle} {family} ({maiden}) {suffix}'``. The quoted nickname round-trips exactly; the parenthesized maiden re-parses as a nickname, a deliberate choice of presentation over lossless round-trip -- use ``née {maiden}`` in a custom spec if you need it to survive a reparse. ``HumanName`` keeps v1's own ``string_format`` default, unchanged + - **Change delimiter-overlap precedence in the 2.0 API:** a pair listed in ``Policy.maiden_delimiters`` is dropped from the effective nickname set, so ``Policy(maiden_delimiters={("(", ")")})`` alone routes parenthesized content to ``maiden``. The default nickname set is exported as ``DEFAULT_NICKNAME_DELIMITERS``. ``HumanName`` keeps v1's nickname-wins precedence, so no existing behavior changes + - **Change suffix-delimiter rendering when a custom suffix delimiter is configured:** for ``suffix_delimiter="/"`` and ``"John Smith, RN/CRNA"``, 1.x split the token and rendered ``suffix="RN, CRNA"`` where 2.0 keeps it whole as ``"RN/CRNA"``. Role assignment is unchanged; only rendering differs + - **Correct a long-standing typo in the shipped vocabulary that 1.x carried:** ``actor`` and ``television`` had trailing spaces in ``TITLES``, so ``"actor" in TITLES`` was ``False``. **Parse output is unchanged** -- the parser normalizes entries on ingest, which is exactly what let the typo go unnoticed -- but code that tests membership against the exported constants directly will see corrected results. The data modules now assert their invariants at import time, alongside the ones ``prefixes.py`` already checked; entries must be stored lowercase and whitespace-free, so this class of typo now fails the build **International name support** - - Add ``nameparser.locales`` with the first two packs: ``locales.RU`` (East Slavic patronymic order) and ``locales.TR_AZ`` (Turkic patronymic markers). Packs are pure data folded in at ``parser_for(locales.RU)``, they compose (``parser_for(locales.RU, locales.TR_AZ)`` unions the rules), and they are never auto-detected -- there is no reliable way to detect a name's language from the name alone. ``locales.available()`` and ``locales.get(code)`` look packs up by code; loading is lazy, so importing the package imports no pack (completes #270; #271/#272/#146 stay staged for 2.x) - - Add non-Latin vocabulary to the default lexicon (#269): Cyrillic, Greek, Arabic and Hebrew titles, conjunctions and name particles. Native-script entries cannot collide with Latin-script names, which is what makes them safe to enable by default. Deferred pending vetting: the Cyrillic ``мл``/``ст`` suffixes and the bare Greek ``κ`` title, which collides with the initial-plus-surname shape. Behavior note: ``محمد بن سلمان`` now chains ``بن`` onto the family name where 1.x read it as a middle name - - Add Arabic-script bound given names (#269): ``عبد``, the kunya pair ``أبو``/``ابو``, and ``أم``/``ام`` join the following word into the given name exactly as their transliterations (``abdul``, ``abu``, ``umm``) always did, so ``عبد الرحمن محمد`` parses given ``عبد الرحمن``, family ``محمد`` where 1.x split it into given plus middle - - Add Arabic honorific titles and the conjunction ``و`` (#269): the doctor, professor, hajj, sheikha and engineer forms (``الدكتور``/``الدكتورة``/``دكتور``/``دكتورة``, ``الأستاذ``/``الأستاذة``/``أستاذ``/``أستاذة``, ``الحاج``/``الحاجة``, ``الشيخة``, ``مهندس``) as given-name titles, since Arabic honorifics precede the given name like ``الشيخ``. Deferred under the collision rule: bare ``سيد``/``شيخ``/``أمير``/``سلطان`` (all common given names), the ``د.`` abbreviation (bare ``د`` would swallow initials), and the Ottoman post-nominals ``باشا``/``بك``/``أفندي`` (which survive as family names) - - Add Hebrew honorifics and post-nominals, and Devanagari titles (#269): the Israeli honorifics ``גברת``, ``פרופ'``/``פרופ׳``, ``פרופסור``, ``עו"ד``/``עו״ד`` and ``הרב`` as titles; ``ז"ל``/``ז״ל`` and ``שליט"א``/``שליט״א`` as suffixes, in both gershayim spellings; and Devanagari ``श्री``, ``श्रीमती`` and ``डॉ``. Latin ``sri``/``shri`` were deliberately not added, because they collide with real given names where the native script cannot. Deferred: bare ``רב`` (an ordinary word meaning "many") and ``בר`` as a particle (Bar is a common modern given name) + - **Add nameparser.locales with the first two packs:** ``locales.RU`` (East Slavic patronymic order) and ``locales.TR_AZ`` (Turkic patronymic markers). Packs are pure data folded in at ``parser_for(locales.RU)``, they compose (``parser_for(locales.RU, locales.TR_AZ)`` unions the rules), and they are never auto-detected -- there is no reliable way to detect a name's language from the name alone. ``locales.available()`` and ``locales.get(code)`` look packs up by code; loading is lazy, so importing the package imports no pack (completes #270; #271/#272/#146 stay staged for 2.x) + - **Add non-Latin vocabulary to the default lexicon (#269):** Cyrillic, Greek, Arabic and Hebrew titles, conjunctions and name particles. Native-script entries cannot collide with Latin-script names, which is what makes them safe to enable by default. Deferred pending vetting: the Cyrillic ``мл``/``ст`` suffixes and the bare Greek ``κ`` title, which collides with the initial-plus-surname shape. Behavior note: ``محمد بن سلمان`` now chains ``بن`` onto the family name where 1.x read it as a middle name + - **Add Arabic-script bound given names (#269):** ``عبد``, the kunya pair ``أبو``/``ابو``, and ``أم``/``ام`` join the following word into the given name exactly as their transliterations (``abdul``, ``abu``, ``umm``) always did, so ``عبد الرحمن محمد`` parses given ``عبد الرحمن``, family ``محمد`` where 1.x split it into given plus middle + - **Add Arabic honorific titles and the conjunction و (#269):** the doctor, professor, hajj, sheikha and engineer forms (``الدكتور``/``الدكتورة``/``دكتور``/``دكتورة``, ``الأستاذ``/``الأستاذة``/``أستاذ``/``أستاذة``, ``الحاج``/``الحاجة``, ``الشيخة``, ``مهندس``) as given-name titles, since Arabic honorifics precede the given name like ``الشيخ``. Deferred under the collision rule: bare ``سيد``/``شيخ``/``أمير``/``سلطان`` (all common given names), the ``د.`` abbreviation (bare ``د`` would swallow initials), and the Ottoman post-nominals ``باشا``/``بك``/``أفندي`` (which survive as family names) + - **Add Hebrew honorifics and post-nominals, and Devanagari titles (#269):** the Israeli honorifics ``גברת``, ``פרופ'``/``פרופ׳``, ``פרופסור``, ``עו"ד``/``עו״ד`` and ``הרב`` as titles; ``ז"ל``/``ז״ל`` and ``שליט"א``/``שליט״א`` as suffixes, in both gershayim spellings; and Devanagari ``श्री``, ``श्रीमती`` and ``डॉ``. Latin ``sri``/``shri`` were deliberately not added, because they collide with real given names where the native script cannot. Deferred: bare ``רב`` (an ordinary word meaning "many") and ``בר`` as a particle (Bar is a common modern given name) **Compatibility layer** - - Reimplement ``HumanName`` as a facade over the 2.0 pipeline, and ``Constants`` as a shim resolving to a ``(Lexicon, Policy)`` snapshot with a shared parser cache. Fields, aggregates, mutation through ``name.C.titles.add(...)``, rendering defaults, ``capitalize()``, ``matches()``, ``comparison_key()``, iteration, ``as_dict()`` and pickling are all preserved, and ``nameparser.parser`` and ``nameparser.config`` remain importable. The compatibility layer ships through 2.x and is removed in 3.0 - - Note that ``CONSTANTS.capitalize_name`` and ``force_mixed_case_capitalization`` are still honored through the facade, but the 2.0 API never capitalizes during ``parse()`` -- call ``capitalized()`` when you want it + - **Reimplement HumanName as a facade over the 2.0 pipeline, and Constants as a shim resolving to a (Lexicon, Policy) snapshot with a shared parser cache.** Fields, aggregates, mutation through ``name.C.titles.add(...)``, rendering defaults, ``capitalize()``, ``matches()``, ``comparison_key()``, iteration, ``as_dict()`` and pickling are all preserved, and ``nameparser.parser`` and ``nameparser.config`` remain importable. The compatibility layer ships through 2.x and is removed in 3.0 + - **Note that CONSTANTS.capitalize_name and force_mixed_case_capitalization are still honored through the facade, but the 2.0 API never capitalizes during parse()** -- call ``capitalized()`` when you want it - Add ``Role`` members (and their string values ``given``/``family``) as valid ``HumanName`` subscript keys: ``hn[Role.GIVEN]`` returns ``hn.first`` - - Add a ``UserWarning`` when assigning ``HumanName.given`` or ``.family``: the facade spells those attributes ``first``/``last``, so the assignment creates an inert stray attribute while the parse keeps the old value. The assignment still happens (v1-legal ad-hoc attributes keep working); the warning names the v1 spelling to use + - **Add a UserWarning when assigning HumanName.given or .family:** the facade spells those attributes ``first``/``last``, so the assignment creates an inert stray attribute while the parse keeps the old value. The assignment still happens (v1-legal ad-hoc attributes keep working); the warning names the v1 spelling to use **Command line** - - Rewrite ``python -m nameparser`` over the 2.0 API with a real argument parser. It prints the parse plus its capitalized form and initials by default, takes ``--json`` to emit the fields as JSON (``python -m nameparser --json "Doe, John"``), takes ``--locale CODE`` to apply a pack, and exits with a usage message on bad arguments + - **Rewrite python -m nameparser over the 2.0 API with a real argument parser.** It prints the parse plus its capitalized form and initials by default, takes ``--json`` to emit the fields as JSON (``python -m nameparser --json "Doe, John"``), takes ``--locale CODE`` to apply a pack, and exits with a usage message on bad arguments **Documentation** - - Rewrite the documentation new-API-first: a new front page and README, ``usage.rst`` as a tour of the 2.0 API, a principle-first ``customize.rst``, a reference split into the 2.0 API and the compatibility layer, and new pages for :doc:`concepts`, :doc:`locales` and :doc:`migrate` -- the last carrying full attribute and configuration maps from v1 names to 2.0 names (#262). The 1.x documentation remains online as the readthedocs ``stable`` build + - **Rewrite the documentation new-API-first:** a new front page and README, ``usage.rst`` as a tour of the 2.0 API, a principle-first ``customize.rst``, a reference split into the 2.0 API and the compatibility layer, and new pages for :doc:`concepts`, :doc:`locales` and :doc:`migrate` -- the last carrying full attribute and configuration maps from v1 names to 2.0 names (#262). The 1.x documentation remains online as the readthedocs ``stable`` build Parsing changes were checked against a 652-name differential corpus -- names harvested from the v1 test banks, plus names reported in @@ -298,30 +298,30 @@ Release Log * 1.4.0 - July 12, 2026 - - Add ``Constants.copy()``, a detached deep copy that preserves the source instance's current customizations (unlike ``Constants()``, which always starts from library defaults) -- useful as ``CONSTANTS.copy()`` for a private snapshot of the shared config (#260) - - Deprecate passing ``constants=None`` to ``HumanName`` (or assigning ``hn.C = None``): it silently builds a fresh ``Constants()``, discarding any customizations the caller may have expected to carry over from the shared ``CONSTANTS``. Emits ``DeprecationWarning``; will raise ``TypeError`` in 2.0. Use ``constants=Constants()`` for fresh library defaults or ``constants=CONSTANTS.copy()`` for a private snapshot instead (closes #260) - - Deprecate assigning ``Constants.empty_attribute_default`` for removal in 2.0 (#255): once ``None`` support goes, the only legal value left is the default ``''``, so a dial with one position isn't configuration. Emits ``DeprecationWarning``; reading the attribute is unaffected - - Deprecate unknown-key attribute access on ``TupleManager``/``RegexTupleManager`` (``CONSTANTS.regexes.typo``, ``CONSTANTS.capitalization_exceptions.typo``, etc.) for removal in 2.0 (#256): a misspelled or omitted key currently degrades silently (``None``/``EMPTY_REGEX``) with no traceback pointing at the typo. Emits ``DeprecationWarning`` naming the miss and the known keys; will raise ``AttributeError`` in 2.0. ``.get()`` remains available for intentional soft access - - Deprecate ``HumanName`` slice access (``name[1:-3]``) and item assignment (``name['first'] = value``) for removal in 2.0 (#258): field access by position has no real use case, and item assignment duplicates plain attribute assignment. Both emit ``DeprecationWarning``; string-key access (``name['first']``) is unaffected - - Deprecate ``SetManager.add_with_encoding()`` itself for removal in 2.0 (#245), regardless of argument type: use ``add()`` instead (decoding bytes first). Previously only the ``bytes`` path warned; the ``str`` path was silent even though the whole method goes away - - Deprecate loading a legacy-format ``Constants`` pickle (written by nameparser <= 1.2.x, before the 1.3.0 pickle fix) for removal in 2.0 (#279): ``__setstate__``'s migration shim currently skips the stale computed-property key silently. Emits ``DeprecationWarning`` once per call telling users to re-pickle; will raise ``ValueError`` in 2.0 + - **Add Constants.copy(), a detached deep copy that preserves the source instance's current customizations (unlike Constants(), which always starts from library defaults)** -- useful as ``CONSTANTS.copy()`` for a private snapshot of the shared config (#260) + - **Deprecate passing constants=None to HumanName (or assigning hn.C = None):** it silently builds a fresh ``Constants()``, discarding any customizations the caller may have expected to carry over from the shared ``CONSTANTS``. Emits ``DeprecationWarning``; will raise ``TypeError`` in 2.0. Use ``constants=Constants()`` for fresh library defaults or ``constants=CONSTANTS.copy()`` for a private snapshot instead (closes #260) + - **Deprecate assigning Constants.empty_attribute_default for removal in 2.0 (#255):** once ``None`` support goes, the only legal value left is the default ``''``, so a dial with one position isn't configuration. Emits ``DeprecationWarning``; reading the attribute is unaffected + - **Deprecate unknown-key attribute access on TupleManager/RegexTupleManager (CONSTANTS.regexes.typo, CONSTANTS.capitalization_exceptions.typo, etc.) for removal in 2.0 (#256):** a misspelled or omitted key currently degrades silently (``None``/``EMPTY_REGEX``) with no traceback pointing at the typo. Emits ``DeprecationWarning`` naming the miss and the known keys; will raise ``AttributeError`` in 2.0. ``.get()`` remains available for intentional soft access + - **Deprecate HumanName slice access (name[1:-3]) and item assignment (name['first'] = value) for removal in 2.0 (#258):** field access by position has no real use case, and item assignment duplicates plain attribute assignment. Both emit ``DeprecationWarning``; string-key access (``name['first']``) is unaffected + - **Deprecate SetManager.add_with_encoding() itself for removal in 2.0 (#245), regardless of argument type:** use ``add()`` instead (decoding bytes first). Previously only the ``bytes`` path warned; the ``str`` path was silent even though the whole method goes away + - **Deprecate loading a legacy-format Constants pickle (written by nameparser <= 1.2.x, before the 1.3.0 pickle fix) for removal in 2.0 (#279):** ``__setstate__``'s migration shim currently skips the stale computed-property key silently. Emits ``DeprecationWarning`` once per call telling users to re-pickle; will raise ``ValueError`` in 2.0 - Fix the ``"Lastname, Firstname"`` comma format not being recognized when the input uses the Arabic comma ``،`` (U+060C, the standard comma in Arabic/Persian/Urdu text) or the fullwidth CJK comma ``,`` (U+FF0C) instead of the ASCII comma: both variants now also split the format and no longer leak into the parsed output (closes #265) * 1.3.1 - July 11, 2026 - Fix invisible Unicode bidirectional control characters (LRM/RLM/ALM, the embedding/override marks, and the isolates U+2066–U+2069) surviving parsing and sticking to ``first``/``last``/etc., so a copy-pasted right-to-left name silently failed equality and dedup. They are now stripped in preprocessing like emoji; disable via ``CONSTANTS.regexes.bidi = False`` (closes #266) - - Fix ``str()`` corrupting name text containing the substring ``"None"`` when ``empty_attribute_default`` is ``None`` (e.g. ``"Nonez Smith"`` rendered as ``"z Smith"``): empty attributes are now substituted as ``''`` before the format string is applied, instead of scrubbing the interpolated ``"None"`` from the output afterward (closes #254) + - **Fix str() corrupting name text containing the substring "None" when empty_attribute_default is None (e.g.** ``"Nonez Smith"`` rendered as ``"z Smith"``): empty attributes are now substituted as ``''`` before the format string is applied, instead of scrubbing the interpolated ``"None"`` from the output afterward (closes #254) * 1.3.0 - July 5, 2026 **Breaking Changes & Deprecations** - - Deprecate ``HumanName.__eq__`` and ``__hash__`` for removal in 2.0 (#223): the current design's three promises — case-insensitive equality, equality with plain strings, and hashability — are mutually inconsistent (equal objects can hash differently), equality depends on ``string_format``, and ``maiden`` is invisible to it. Both now emit ``DeprecationWarning`` naming the replacement; behavior is otherwise unchanged until 2.0 (closes #224) - - Deprecate ``bytes`` input for removal in 2.0 (#245): passing ``bytes`` to ``HumanName``/``full_name`` or to ``SetManager.add()``/``add_with_encoding()`` now emits ``DeprecationWarning`` — decode first, e.g. ``value.decode('utf-8')``. The ``encoding`` constructor argument is deprecated with it - - Deprecate ``SetManager.__call__`` for removal in 2.0 (#243): calling a manager returns the raw underlying set, so mutating the result bypasses normalization and cache invalidation; iterate the manager or copy with ``set(manager)`` instead - - Add ``SetManager.discard()``, and deprecate ``remove()`` of a *missing* member (#243): it currently does nothing but will raise ``KeyError`` in 2.0, matching ``set.remove``; use ``discard()`` for intentional ignore-missing removal. Removing present members is unchanged and does not warn - - Fix ``HumanName`` acting as its own iterator with a stored cursor: breaking out of a loop, iterating in nested loops, or calling ``len(name)`` mid-loop corrupted subsequent iteration; ``iter(name)`` now returns a fresh independent iterator each time. ``next(name)`` on the instance itself (undocumented) now raises ``TypeError`` — call ``next(iter(name))`` instead (closes #225) - - Remove the vestigial ``unparsable`` attribute: the guard that was meant to set it has been unreachable since 2013 (v0.2.9), so it has reported ``False`` for every parsed name for over a decade; check ``len(name) == 0`` to detect an empty parse + - **Deprecate HumanName.__eq__ and __hash__ for removal in 2.0 (#223):** the current design's three promises — case-insensitive equality, equality with plain strings, and hashability — are mutually inconsistent (equal objects can hash differently), equality depends on ``string_format``, and ``maiden`` is invisible to it. Both now emit ``DeprecationWarning`` naming the replacement; behavior is otherwise unchanged until 2.0 (closes #224) + - **Deprecate bytes input for removal in 2.0 (#245):** passing ``bytes`` to ``HumanName``/``full_name`` or to ``SetManager.add()``/``add_with_encoding()`` now emits ``DeprecationWarning`` — decode first, e.g. ``value.decode('utf-8')``. The ``encoding`` constructor argument is deprecated with it + - **Deprecate SetManager.__call__ for removal in 2.0 (#243):** calling a manager returns the raw underlying set, so mutating the result bypasses normalization and cache invalidation; iterate the manager or copy with ``set(manager)`` instead + - **Add SetManager.discard(), and deprecate remove() of a missing member (#243):** it currently does nothing but will raise ``KeyError`` in 2.0, matching ``set.remove``; use ``discard()`` for intentional ignore-missing removal. Removing present members is unchanged and does not warn + - **Fix HumanName acting as its own iterator with a stored cursor:** breaking out of a loop, iterating in nested loops, or calling ``len(name)`` mid-loop corrupted subsequent iteration; ``iter(name)`` now returns a fresh independent iterator each time. ``next(name)`` on the instance itself (undocumented) now raises ``TypeError`` — call ``next(iter(name))`` instead (closes #225) + - **Remove the vestigial unparsable attribute:** the guard that was meant to set it has been unreachable since 2013 (v0.2.9), so it has reported ``False`` for every parsed name for over a decade; check ``len(name) == 0`` to detect an empty parse - Remove ``__ne__``; Python 3 derives ``!=`` from ``__eq__`` automatically - Change internal initials helper ``__process_initial__`` to ``_process_initial``: double-underscore-both-sides names are reserved for Python special methods; subclasses overriding the old name must rename their override - Change ``REGEXES`` from a ``set`` of ``(name, pattern)`` tuples to a ``dict``, so a duplicate name is a visible overwrite in the source instead of a nondeterministic winner at import time; code iterating ``REGEXES`` directly now gets keys instead of pairs — use ``.items()`` (#227) @@ -335,17 +335,17 @@ Release Log ``middle="ahmed"``, ``last="salem"``). Disable via ``CONSTANTS.bound_first_names.clear()``. **Default-on: changes parsing output for names with these prefixes.** (#150) - - Treat an unrecognized, multi-letter token ending in a period in the leading title run (before the first name is set), e.g. ``"Major."``, as a ``title`` instead of a ``first`` name; internal-period abbreviations (``"E.T."``) and single-letter initials (``"J."``) are unaffected. **Default-on: changes parsing of names with a leading unknown period-abbreviation** (closes #109) - - Fix parsing writing back into the ``Constants`` it reads (usually the shared module-level ``CONSTANTS``): pieces derived while parsing a name — period-joined titles/suffixes like ``"Lt.Gov."`` and conjunction-joined pieces like ``"Mr. and Mrs."`` or ``"von und zu"`` — are now tracked per parse instead of being permanently ``add()``-ed to the config, so parse results no longer depend on which names were parsed earlier in the process and parsing no longer mutates shared state across threads + - **Treat an unrecognized, multi-letter token ending in a period in the leading title run (before the first name is set), e.g.** ``"Major."``, as a ``title`` instead of a ``first`` name; internal-period abbreviations (``"E.T."``) and single-letter initials (``"J."``) are unaffected. **Default-on: changes parsing of names with a leading unknown period-abbreviation** (closes #109) + - **Fix parsing writing back into the Constants it reads (usually the shared module-level CONSTANTS):** pieces derived while parsing a name — period-joined titles/suffixes like ``"Lt.Gov."`` and conjunction-joined pieces like ``"Mr. and Mrs."`` or ``"von und zu"`` — are now tracked per parse instead of being permanently ``add()``-ed to the config, so parse results no longer depend on which names were parsed earlier in the process and parsing no longer mutates shared state across threads - Fix ``__hash__`` to lowercase the name like ``__eq__`` does, so equal ``HumanName`` instances hash equal and behave correctly in sets and dicts **New comparison methods** - - Add ``matches()`` and ``comparison_key()`` for explicit name comparison: ``matches()`` compares parsed components case-insensitively (parsing ``str`` arguments first, so ``name.matches("Smith, John")`` and ``name.matches("John Smith")`` both match) and ``comparison_key()`` returns a hashable tuple of the seven components for dedup, dict keys, and sorting (#224) + - **Add matches() and comparison_key() for explicit name comparison:** ``matches()`` compares parsed components case-insensitively (parsing ``str`` arguments first, so ``name.matches("Smith, John")`` and ``name.matches("John Smith")`` both match) and ``comparison_key()`` returns a hashable tuple of the seven components for dedup, dict keys, and sorting (#224) **New name fields** - - Add a first-class ``maiden`` field and ``maiden_delimiters`` to ``Constants``, so a delimiter (e.g. parenthesis) can be routed to ``maiden`` instead of ``nickname`` for alternate/maiden surnames, e.g. ``"Baker (Johnson), Jenny"`` (closes #22) + - **Add a first-class maiden field and maiden_delimiters to Constants, so a delimiter (e.g.** parenthesis) can be routed to ``maiden`` instead of ``nickname`` for alternate/maiden surnames, e.g. ``"Baker (Johnson), Jenny"`` (closes #22) - Add ``given_names`` (and ``given_names_list``) attribute as aggregate of first and middle names, mirroring ``surnames`` (closes #157) - Add ``last_base``, ``last_prefixes`` (and ``_list`` variants) for splitting last-name prefix particles (tussenvoegsels) from the core surname (#130, #132) @@ -360,18 +360,18 @@ Release Log - Add ``patronymic_name_order`` flag to ``Constants`` and ``HumanName`` for opt-in detection and reordering of Russian formal-order names (Surname GivenName Patronymic) (#85) - Add Turkic (Azerbaijani/Central-Asian) patronymic detection to ``patronymic_name_order``, rotating the reversed 4-token formal shape (``Surname GivenName PatronymicRoot Marker``, e.g. ``oglu``/``qizi``) into Western order (#185) - - Add ``middle_name_as_last`` flag to ``Constants`` and ``HumanName`` for opt-in folding of middle names into the last name, for naming systems with no middle-name concept (e.g. Arabic patronymic chaining) (#133) - - Add ``non_first_name_prefixes`` to ``Constants``: a leading particle that is never a first name (e.g. ``"de Mesnil"``, ``"dos Santos"``) now parses as a surname with an empty first name, instead of treating the particle as the first name (closes #121) + - **Add middle_name_as_last flag to Constants and HumanName for opt-in folding of middle names into the last name, for naming systems with no middle-name concept (e.g.** Arabic patronymic chaining) (#133) + - **Add non_first_name_prefixes to Constants:** a leading particle that is never a first name (e.g. ``"de Mesnil"``, ``"dos Santos"``) now parses as a surname with an empty first name, instead of treating the particle as the first name (closes #121) - Add international honorifics to ``TITLES`` (#187) - Add German/Austrian nobility and ecclesiastical titles to ``TITLES`` (closes #101) - - Add German/Dutch last-name prefixes and title/degree suffixes; fix ``join_on_conjunctions()`` to register multi-word prefix chains (e.g. ``"von und zu"``) as prefixes, mirroring existing title handling (closes #18) + - **Add German/Dutch last-name prefixes and title/degree suffixes; fix join_on_conjunctions() to register multi-word prefix chains (e.g.** ``"von und zu"``) as prefixes, mirroring existing title handling (closes #18) **Parsing fixes** - - Fix suffix boundary lookup for prefixed last names with a title before and after (e.g. ``"dr Vincent van Gogh dr"`` producing a corrupted middle name) (closes #100) - - Fix a repeated prefix word in a prefix chain (e.g. ``"Juan de la de la Vega"``) silently dropping the earlier occurrence in ``join_on_conjunctions()``: value-based ``pieces.index(prefix)`` lookups re-found the wrong occurrence once the list had already been mutated by prior joins; prefix positions are now tracked positionally instead of re-derived by value (closes #208) + - **Fix suffix boundary lookup for prefixed last names with a title before and after (e.g.** ``"dr Vincent van Gogh dr"`` producing a corrupted middle name) (closes #100) + - **Fix a repeated prefix word in a prefix chain (e.g.** ``"Juan de la de la Vega"``) silently dropping the earlier occurrence in ``join_on_conjunctions()``: value-based ``pieces.index(prefix)`` lookups re-found the wrong occurrence once the list had already been mutated by prior joins; prefix positions are now tracked positionally instead of re-derived by value (closes #208) - Fix a trailing suffix being silently dropped after an empty comma segment, e.g. ``"Doe, John,, Jr."`` losing the ``"Jr."`` - - Fix degenerate comma input (a bare ``","`` or an empty comma segment, e.g. ``"Doe,, Jr."``, ``"John Doe, Jr.,,"``) leaving an empty-string member in ``first_list``, ``last_list``, or ``suffix_list``; whitespace-only tokens assigned via the setters are dropped the same way + - **Fix degenerate comma input (a bare "," or an empty comma segment, e.g.** ``"Doe,, Jr."``, ``"John Doe, Jr.,,"``) leaving an empty-string member in ``first_list``, ``last_list``, or ``suffix_list``; whitespace-only tokens assigned via the setters are dropped the same way - Fix suffix-shaped parenthesized/quoted content (e.g. ``"(Ret)"``, ``"(MBA)"``) being misclassified as a nickname instead of a suffix (closes #111) - Fix single-character symbol conjunctions (e.g. ``"&"``, ``"/"``) being ignored in short names (#173) - Fix recognition of single-letter roman numeral suffixes (e.g. ``"I"``, ``"V"``) in suffix-comma format (closes #136) @@ -382,27 +382,27 @@ Release Log **Formatting and output fixes** - Fix ``IndexError`` in ``initials()``/``initials_list()`` when a ``*_list`` attribute was assigned directly with an element containing unnormalized whitespace (e.g. ``name.middle_list = ['Q R']``), bypassing the parser's whitespace normalization (closes #232) - - Fix ``initials()`` emitting a stray empty initial (e.g. ``"J. . V."``) -- or raising ``TypeError`` when ``empty_attribute_default`` is ``None`` -- for name parts with no initialable words, e.g. a prefix-only middle name like ``"de la"`` + - **Fix initials() emitting a stray empty initial (e.g.** ``"J. . V."``) -- or raising ``TypeError`` when ``empty_attribute_default`` is ``None`` -- for name parts with no initialable words, e.g. a prefix-only middle name like ``"de la"`` - Fix capitalization of suffix acronyms written with dots, e.g. ``"M.D."`` (closes #141) - Fix extra whitespace before punctuation in ``str()`` output when a ``string_format`` field is empty (closes #139) - Fix spurious leading space in surnames and empty token in suffix list after ``capitalize()`` with an empty middle or suffix (#164) **API correctness and cleanup** - - Fix the five non-cached-union ``SetManager``-backed ``Constants`` attributes (``first_name_titles``, ``conjunctions``, ``bound_first_names``, ``non_first_name_prefixes``, ``suffix_acronyms_ambiguous``) accepting non-``SetManager`` assignment silently (e.g. ``constants.conjunctions = 'and'``), degrading membership checks into substring tests with no error; assignment now raises ``TypeError`` like the four cached-union attributes already did (closes #241) - - Fix ``HumanName.C`` accepting an invalid ``constants`` value on post-construction assignment (e.g. ``hn.C = 'garbage'``), bypassing the constructor's validation and failing later with an unrelated ``AttributeError``; ``C`` is now a property that validates on assignment too (closes #239) + - **Fix the five non-cached-union SetManager-backed Constants attributes (first_name_titles, conjunctions, bound_first_names, non_first_name_prefixes, suffix_acronyms_ambiguous) accepting non-SetManager assignment silently (e.g.** ``constants.conjunctions = 'and'``), degrading membership checks into substring tests with no error; assignment now raises ``TypeError`` like the four cached-union attributes already did (closes #241) + - **Fix HumanName.C accepting an invalid constants value on post-construction assignment (e.g.** ``hn.C = 'garbage'``), bypassing the constructor's validation and failing later with an unrelated ``AttributeError``; ``C`` is now a property that validates on assignment too (closes #239) - Fix ``TupleManager`` (and ``RegexTupleManager``) accepting a bare string/bytes argument (raising a cryptic ``dict``-internals ``ValueError``) or an iterable of 2-character strings (silently shredding each into a key/value pair, e.g. ``Constants(capitalization_exceptions=['ii'])`` becoming ``{'i': 'i'}``); both now raise ``TypeError`` with a clear message (closes #242) - - Fix ``SetManager.__contains__`` being the one operation that didn't normalize (lowercase, strip leading/trailing periods) its operand, so e.g. ``'Dr.' in constants.titles`` could return ``False`` even though the title was correctly configured; membership checks now normalize like ``add()``/``remove()``/the constructor/the set operators (closes #244) - - Fix a bare string passed to a set-backed ``Constants`` argument (e.g. ``Constants(titles='dr')``), to ``SetManager``, or as a ``SetManager`` set-operator operand (e.g. ``constants.titles |= 'esq'``) being silently split into single characters, replacing or polluting the set and producing wrong parses with no error; it now raises ``TypeError`` with the suggested fix — wrap strings in a list, decode ``bytes`` first (closes #238) - - Fix ``SetManager`` set operators and the constructor skipping the lowercase/strip-edge-periods normalization that ``add()`` applies: ``constants.titles |= ['Esq.']`` kept a raw ``'Esq.'`` the parser's lookups could never match, ``titles & ['Dr.']`` missed ``'dr'``, and ``Constants(titles=[...])`` stored raw elements that silently never matched; elements and operands are now normalized everywhere, and non-``str`` elements (``bytes``, ``None``, numbers) raise ``TypeError`` instead of crashing cryptically or being coerced - - Fix the ``constants`` constructor argument silently discarding ``Constants`` *subclass* instances: the exact-type check replaced them with fresh defaults, throwing away the caller's configuration. Subclass instances are now used as given; anything that is neither ``None`` nor a ``Constants`` instance now raises ``TypeError`` instead of being silently swapped for defaults (closes #226) + - **Fix SetManager.__contains__ being the one operation that didn't normalize (lowercase, strip leading/trailing periods) its operand, so e.g.** ``'Dr.' in constants.titles`` could return ``False`` even though the title was correctly configured; membership checks now normalize like ``add()``/``remove()``/the constructor/the set operators (closes #244) + - **Fix a bare string passed to a set-backed Constants argument (e.g.** ``Constants(titles='dr')``), to ``SetManager``, or as a ``SetManager`` set-operator operand (e.g. ``constants.titles |= 'esq'``) being silently split into single characters, replacing or polluting the set and producing wrong parses with no error; it now raises ``TypeError`` with the suggested fix — wrap strings in a list, decode ``bytes`` first (closes #238) + - **Fix SetManager set operators and the constructor skipping the lowercase/strip-edge-periods normalization that add() applies:** ``constants.titles |= ['Esq.']`` kept a raw ``'Esq.'`` the parser's lookups could never match, ``titles & ['Dr.']`` missed ``'dr'``, and ``Constants(titles=[...])`` stored raw elements that silently never matched; elements and operands are now normalized everywhere, and non-``str`` elements (``bytes``, ``None``, numbers) raise ``TypeError`` instead of crashing cryptically or being coerced + - **Fix the constants constructor argument silently discarding Constants subclass instances:** the exact-type check replaced them with fresh defaults, throwing away the caller's configuration. Subclass instances are now used as given; anything that is neither ``None`` nor a ``Constants`` instance now raises ``TypeError`` instead of being silently swapped for defaults (closes #226) - Fix ``Constants`` customizations, singleton identity, and ``TupleManager`` subclass being lost across ``pickle``/``deepcopy`` round-trips (#167, #168, #169) - Fix ``is_rootname()`` returning stale results after ``add()``/``remove()`` on ``titles``, ``prefixes``, ``suffix_acronyms``, or ``suffix_not_acronyms`` (#166) - Fix the library logger calling ``setLevel(logging.ERROR)`` on import, which silently discarded log records regardless of an application's own logging configuration; the logger now leaves its level at ``NOTSET`` and lets the application control verbosity (closes #228) - - Minor internal cleanups: drop a dead length check in the initials helper, simplify double-wrapped ``len(list(...))`` calls, and other small parser tidy-ups with no behavior change (closes #229) + - **Minor internal cleanups:** drop a dead length check in the initials helper, simplify double-wrapped ``len(list(...))`` calls, and other small parser tidy-ups with no behavior change (closes #229) - Change ``Constants.__repr__`` to report collection sizes and non-default scalar config, replacing the uninformative ```` (#221) * 1.2.1 - June 19, 2026 - - Fix ``initials()`` interpolating the literal ``None`` for empty name parts when ``empty_attribute_default = None`` (e.g. ``"J. None D."``); empty parts now render as an empty string and a fully-empty result returns ``empty_attribute_default`` + - **Fix initials() interpolating the literal None for empty name parts when empty_attribute_default = None (e.g.** ``"J. None D."``); empty parts now render as an empty string and a fully-empty result returns ``empty_attribute_default`` - Add ``python -m nameparser "Name String"`` command-line helper that prints a parsed name - Reorganize the test suite from a single ``tests.py`` into a ``tests/`` pytest package * 1.2.0 - June 11, 2026 @@ -533,7 +533,7 @@ Release Log - use unicode_literals to simplify Python 2 & 3 support. - Generate documentation using sphinx and host on readthedocs. * 0.2.10 - May 6, 2014 - - If name is only a title and one part, assume it's a last name instead of a first name, with exceptions for some titles like 'Sir'. (`#7 `_). + - **If name is only a title and one part, assume it's a last name instead of a first name, with exceptions for some titles like 'Sir'.** (`#7 `_). - Add some judicial and other common titles. (#9) * 0.2.9 - Apr 1, 2014 - Add a new nickname attribute containing anything in parenthesis or double quotes (`Issue 33 `_). From 8a9043651ca9c85080d36040878d3c0ffd024922 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Mon, 31 Aug 2026 19:59:03 -0700 Subject: [PATCH 07/10] docs(release_log): don't cut a bold lead at an abbreviation's period Derek caught the 1.3.0 bullets ending mid-parenthesis: **Add middle_name_as_last flag ... with no middle-name concept (e.g.** Arabic patronymic chaining) (#133) The splitter masked ``literals`` before looking for a sentence end but knew nothing about plain-text abbreviations, so "e.g." read as one. Fourteen leads were truncated, every one of them in 1.3.x, where "(e.g. X)" is the house idiom for an example. Fixed at the cause rather than in the fourteen places: the splitter now walks EVERY candidate cut in order and takes the first VALID one, where valid means the lead does not end in a known abbreviation and does not stop inside an unpaired bracket or quote. Where no cut qualifies the bullet is left unbolded, which is the right answer for these: each is a single sentence, so its "lead" would have been the whole bullet. 123 leads rather than 134; the 11 that dropped out are exactly the ones that were being truncated. Better plain than cut off mid-parenthesis. Verified on the rendered HTML across all three failure classes, not just the one reported -- of 170 spans, zero contain stray markup, zero end in an abbreviation, zero hold an unbalanced bracket or quote. The first two of those checks did not exist before this commit, which is why the defect shipped: I had checked the nesting class and assumed the splitting was right. Co-Authored-By: Claude Opus 5 --- docs/release_log.rst | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/release_log.rst b/docs/release_log.rst index 183e4be5..cf51103a 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -310,7 +310,7 @@ Release Log * 1.3.1 - July 11, 2026 - Fix invisible Unicode bidirectional control characters (LRM/RLM/ALM, the embedding/override marks, and the isolates U+2066–U+2069) surviving parsing and sticking to ``first``/``last``/etc., so a copy-pasted right-to-left name silently failed equality and dedup. They are now stripped in preprocessing like emoji; disable via ``CONSTANTS.regexes.bidi = False`` (closes #266) - - **Fix str() corrupting name text containing the substring "None" when empty_attribute_default is None (e.g.** ``"Nonez Smith"`` rendered as ``"z Smith"``): empty attributes are now substituted as ``''`` before the format string is applied, instead of scrubbing the interpolated ``"None"`` from the output afterward (closes #254) + - **Fix str() corrupting name text containing the substring "None" when empty_attribute_default is None (e.g. "Nonez Smith" rendered as "z Smith"):** empty attributes are now substituted as ``''`` before the format string is applied, instead of scrubbing the interpolated ``"None"`` from the output afterward (closes #254) * 1.3.0 - July 5, 2026 @@ -335,7 +335,7 @@ Release Log ``middle="ahmed"``, ``last="salem"``). Disable via ``CONSTANTS.bound_first_names.clear()``. **Default-on: changes parsing output for names with these prefixes.** (#150) - - **Treat an unrecognized, multi-letter token ending in a period in the leading title run (before the first name is set), e.g.** ``"Major."``, as a ``title`` instead of a ``first`` name; internal-period abbreviations (``"E.T."``) and single-letter initials (``"J."``) are unaffected. **Default-on: changes parsing of names with a leading unknown period-abbreviation** (closes #109) + - Treat an unrecognized, multi-letter token ending in a period in the leading title run (before the first name is set), e.g. ``"Major."``, as a ``title`` instead of a ``first`` name; internal-period abbreviations (``"E.T."``) and single-letter initials (``"J."``) are unaffected. **Default-on: changes parsing of names with a leading unknown period-abbreviation** (closes #109) - **Fix parsing writing back into the Constants it reads (usually the shared module-level CONSTANTS):** pieces derived while parsing a name — period-joined titles/suffixes like ``"Lt.Gov."`` and conjunction-joined pieces like ``"Mr. and Mrs."`` or ``"von und zu"`` — are now tracked per parse instead of being permanently ``add()``-ed to the config, so parse results no longer depend on which names were parsed earlier in the process and parsing no longer mutates shared state across threads - Fix ``__hash__`` to lowercase the name like ``__eq__`` does, so equal ``HumanName`` instances hash equal and behave correctly in sets and dicts @@ -345,7 +345,7 @@ Release Log **New name fields** - - **Add a first-class maiden field and maiden_delimiters to Constants, so a delimiter (e.g.** parenthesis) can be routed to ``maiden`` instead of ``nickname`` for alternate/maiden surnames, e.g. ``"Baker (Johnson), Jenny"`` (closes #22) + - Add a first-class ``maiden`` field and ``maiden_delimiters`` to ``Constants``, so a delimiter (e.g. parenthesis) can be routed to ``maiden`` instead of ``nickname`` for alternate/maiden surnames, e.g. ``"Baker (Johnson), Jenny"`` (closes #22) - Add ``given_names`` (and ``given_names_list``) attribute as aggregate of first and middle names, mirroring ``surnames`` (closes #157) - Add ``last_base``, ``last_prefixes`` (and ``_list`` variants) for splitting last-name prefix particles (tussenvoegsels) from the core surname (#130, #132) @@ -360,18 +360,18 @@ Release Log - Add ``patronymic_name_order`` flag to ``Constants`` and ``HumanName`` for opt-in detection and reordering of Russian formal-order names (Surname GivenName Patronymic) (#85) - Add Turkic (Azerbaijani/Central-Asian) patronymic detection to ``patronymic_name_order``, rotating the reversed 4-token formal shape (``Surname GivenName PatronymicRoot Marker``, e.g. ``oglu``/``qizi``) into Western order (#185) - - **Add middle_name_as_last flag to Constants and HumanName for opt-in folding of middle names into the last name, for naming systems with no middle-name concept (e.g.** Arabic patronymic chaining) (#133) + - Add ``middle_name_as_last`` flag to ``Constants`` and ``HumanName`` for opt-in folding of middle names into the last name, for naming systems with no middle-name concept (e.g. Arabic patronymic chaining) (#133) - **Add non_first_name_prefixes to Constants:** a leading particle that is never a first name (e.g. ``"de Mesnil"``, ``"dos Santos"``) now parses as a surname with an empty first name, instead of treating the particle as the first name (closes #121) - Add international honorifics to ``TITLES`` (#187) - Add German/Austrian nobility and ecclesiastical titles to ``TITLES`` (closes #101) - - **Add German/Dutch last-name prefixes and title/degree suffixes; fix join_on_conjunctions() to register multi-word prefix chains (e.g.** ``"von und zu"``) as prefixes, mirroring existing title handling (closes #18) + - Add German/Dutch last-name prefixes and title/degree suffixes; fix ``join_on_conjunctions()`` to register multi-word prefix chains (e.g. ``"von und zu"``) as prefixes, mirroring existing title handling (closes #18) **Parsing fixes** - - **Fix suffix boundary lookup for prefixed last names with a title before and after (e.g.** ``"dr Vincent van Gogh dr"`` producing a corrupted middle name) (closes #100) - - **Fix a repeated prefix word in a prefix chain (e.g.** ``"Juan de la de la Vega"``) silently dropping the earlier occurrence in ``join_on_conjunctions()``: value-based ``pieces.index(prefix)`` lookups re-found the wrong occurrence once the list had already been mutated by prior joins; prefix positions are now tracked positionally instead of re-derived by value (closes #208) + - Fix suffix boundary lookup for prefixed last names with a title before and after (e.g. ``"dr Vincent van Gogh dr"`` producing a corrupted middle name) (closes #100) + - **Fix a repeated prefix word in a prefix chain (e.g. "Juan de la de la Vega") silently dropping the earlier occurrence in join_on_conjunctions():** value-based ``pieces.index(prefix)`` lookups re-found the wrong occurrence once the list had already been mutated by prior joins; prefix positions are now tracked positionally instead of re-derived by value (closes #208) - Fix a trailing suffix being silently dropped after an empty comma segment, e.g. ``"Doe, John,, Jr."`` losing the ``"Jr."`` - - **Fix degenerate comma input (a bare "," or an empty comma segment, e.g.** ``"Doe,, Jr."``, ``"John Doe, Jr.,,"``) leaving an empty-string member in ``first_list``, ``last_list``, or ``suffix_list``; whitespace-only tokens assigned via the setters are dropped the same way + - Fix degenerate comma input (a bare ``","`` or an empty comma segment, e.g. ``"Doe,, Jr."``, ``"John Doe, Jr.,,"``) leaving an empty-string member in ``first_list``, ``last_list``, or ``suffix_list``; whitespace-only tokens assigned via the setters are dropped the same way - Fix suffix-shaped parenthesized/quoted content (e.g. ``"(Ret)"``, ``"(MBA)"``) being misclassified as a nickname instead of a suffix (closes #111) - Fix single-character symbol conjunctions (e.g. ``"&"``, ``"/"``) being ignored in short names (#173) - Fix recognition of single-letter roman numeral suffixes (e.g. ``"I"``, ``"V"``) in suffix-comma format (closes #136) @@ -382,18 +382,18 @@ Release Log **Formatting and output fixes** - Fix ``IndexError`` in ``initials()``/``initials_list()`` when a ``*_list`` attribute was assigned directly with an element containing unnormalized whitespace (e.g. ``name.middle_list = ['Q R']``), bypassing the parser's whitespace normalization (closes #232) - - **Fix initials() emitting a stray empty initial (e.g.** ``"J. . V."``) -- or raising ``TypeError`` when ``empty_attribute_default`` is ``None`` -- for name parts with no initialable words, e.g. a prefix-only middle name like ``"de la"`` + - **Fix initials() emitting a stray empty initial (e.g. "J. . V.")** -- or raising ``TypeError`` when ``empty_attribute_default`` is ``None`` -- for name parts with no initialable words, e.g. a prefix-only middle name like ``"de la"`` - Fix capitalization of suffix acronyms written with dots, e.g. ``"M.D."`` (closes #141) - Fix extra whitespace before punctuation in ``str()`` output when a ``string_format`` field is empty (closes #139) - Fix spurious leading space in surnames and empty token in suffix list after ``capitalize()`` with an empty middle or suffix (#164) **API correctness and cleanup** - - **Fix the five non-cached-union SetManager-backed Constants attributes (first_name_titles, conjunctions, bound_first_names, non_first_name_prefixes, suffix_acronyms_ambiguous) accepting non-SetManager assignment silently (e.g.** ``constants.conjunctions = 'and'``), degrading membership checks into substring tests with no error; assignment now raises ``TypeError`` like the four cached-union attributes already did (closes #241) - - **Fix HumanName.C accepting an invalid constants value on post-construction assignment (e.g.** ``hn.C = 'garbage'``), bypassing the constructor's validation and failing later with an unrelated ``AttributeError``; ``C`` is now a property that validates on assignment too (closes #239) + - Fix the five non-cached-union ``SetManager``-backed ``Constants`` attributes (``first_name_titles``, ``conjunctions``, ``bound_first_names``, ``non_first_name_prefixes``, ``suffix_acronyms_ambiguous``) accepting non-``SetManager`` assignment silently (e.g. ``constants.conjunctions = 'and'``), degrading membership checks into substring tests with no error; assignment now raises ``TypeError`` like the four cached-union attributes already did (closes #241) + - Fix ``HumanName.C`` accepting an invalid ``constants`` value on post-construction assignment (e.g. ``hn.C = 'garbage'``), bypassing the constructor's validation and failing later with an unrelated ``AttributeError``; ``C`` is now a property that validates on assignment too (closes #239) - Fix ``TupleManager`` (and ``RegexTupleManager``) accepting a bare string/bytes argument (raising a cryptic ``dict``-internals ``ValueError``) or an iterable of 2-character strings (silently shredding each into a key/value pair, e.g. ``Constants(capitalization_exceptions=['ii'])`` becoming ``{'i': 'i'}``); both now raise ``TypeError`` with a clear message (closes #242) - - **Fix SetManager.__contains__ being the one operation that didn't normalize (lowercase, strip leading/trailing periods) its operand, so e.g.** ``'Dr.' in constants.titles`` could return ``False`` even though the title was correctly configured; membership checks now normalize like ``add()``/``remove()``/the constructor/the set operators (closes #244) - - **Fix a bare string passed to a set-backed Constants argument (e.g.** ``Constants(titles='dr')``), to ``SetManager``, or as a ``SetManager`` set-operator operand (e.g. ``constants.titles |= 'esq'``) being silently split into single characters, replacing or polluting the set and producing wrong parses with no error; it now raises ``TypeError`` with the suggested fix — wrap strings in a list, decode ``bytes`` first (closes #238) + - Fix ``SetManager.__contains__`` being the one operation that didn't normalize (lowercase, strip leading/trailing periods) its operand, so e.g. ``'Dr.' in constants.titles`` could return ``False`` even though the title was correctly configured; membership checks now normalize like ``add()``/``remove()``/the constructor/the set operators (closes #244) + - Fix a bare string passed to a set-backed ``Constants`` argument (e.g. ``Constants(titles='dr')``), to ``SetManager``, or as a ``SetManager`` set-operator operand (e.g. ``constants.titles |= 'esq'``) being silently split into single characters, replacing or polluting the set and producing wrong parses with no error; it now raises ``TypeError`` with the suggested fix — wrap strings in a list, decode ``bytes`` first (closes #238) - **Fix SetManager set operators and the constructor skipping the lowercase/strip-edge-periods normalization that add() applies:** ``constants.titles |= ['Esq.']`` kept a raw ``'Esq.'`` the parser's lookups could never match, ``titles & ['Dr.']`` missed ``'dr'``, and ``Constants(titles=[...])`` stored raw elements that silently never matched; elements and operands are now normalized everywhere, and non-``str`` elements (``bytes``, ``None``, numbers) raise ``TypeError`` instead of crashing cryptically or being coerced - **Fix the constants constructor argument silently discarding Constants subclass instances:** the exact-type check replaced them with fresh defaults, throwing away the caller's configuration. Subclass instances are now used as given; anything that is neither ``None`` nor a ``Constants`` instance now raises ``TypeError`` instead of being silently swapped for defaults (closes #226) - Fix ``Constants`` customizations, singleton identity, and ``TupleManager`` subclass being lost across ``pickle``/``deepcopy`` round-trips (#167, #168, #169) @@ -402,7 +402,7 @@ Release Log - **Minor internal cleanups:** drop a dead length check in the initials helper, simplify double-wrapped ``len(list(...))`` calls, and other small parser tidy-ups with no behavior change (closes #229) - Change ``Constants.__repr__`` to report collection sizes and non-default scalar config, replacing the uninformative ```` (#221) * 1.2.1 - June 19, 2026 - - **Fix initials() interpolating the literal None for empty name parts when empty_attribute_default = None (e.g.** ``"J. None D."``); empty parts now render as an empty string and a fully-empty result returns ``empty_attribute_default`` + - Fix ``initials()`` interpolating the literal ``None`` for empty name parts when ``empty_attribute_default = None`` (e.g. ``"J. None D."``); empty parts now render as an empty string and a fully-empty result returns ``empty_attribute_default`` - Add ``python -m nameparser "Name String"`` command-line helper that prints a parsed name - Reorganize the test suite from a single ``tests.py`` into a ``tests/`` pytest package * 1.2.0 - June 11, 2026 From 02dd54b373a42f04029c147a7d3f263157ee54be Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Mon, 31 Aug 2026 20:27:51 -0700 Subject: [PATCH 08/10] docs(release_log): cut the noise from the 2.2 bullets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Derek's read, and the specific cuts are his: - the z-domu migration call-out ("if you stored z and domu separately, remove both") -- nobody did that, so it is advice to no one - "a clause holding nothing but the marker stays a nickname" - the roz lead's reason clause ("where it had been eating English given names") -- the removal is the point - "what stops working is the abbreviation itself, for Czech and Slovak callers", with its worked example and 1.4.0 comparison And the style problem, which was real: "What does NOT move: a lone marker in brackets is just a word ..." promises a list of things that do not move and then delivers a statement about what something IS. Four long sentences to establish what "Jane Smith (née)" does, which is not worth the reading. Replaced by the one clause a caller acts on -- a markerless clause is still a nickname, which is what maiden_delimiters remains for. Then the same standard applied across the section, 32 cuts in all. The classes: a non-change nobody would have assumed otherwise ("Names without the comma are untouched", "Jack Mc Donald is byte-identical", "a conjunction written as a word of its own is untouched"); an edge case too narrow to earn its sentence (the trailing bare marker, "Smith née Jones PhD"); rationale the reader does not need to act ("it was never a reliable way to change a default ..."); corpus and vocabulary trivia ("eight more corpus names", "membership goes from 28 words to 33"); and CI forensics that belong in the decisions entry the bullet already cites. 4927 -> 4456 words. Every cut is a deletion of a whole sentence or clause; the 21 in-place rewordings were diffed word by word against the intended list, and no fact left the file that was not deliberately dropped. ONE SELF-INFLICTED SCARE, recorded because the check is the lesson: a blanket ' +\.' -> '.' tidy meant to clean up after the deletions ate the space in `from ... import` (x3) and in two deliberate examples, `"John . Smith"` and `"J. . V."` -- an ellipsis and a spaced initial both carry a legitimate space before a period. Caught by diffing every changed line word-for-word rather than by reading the result. Reverted and redone with the tidy narrowed to collapsing double spaces. Verified: 169 bold leads with zero stray markup, zero abbreviation truncations, zero unbalanced brackets; sphinx html and doctest clean; 6196 tests pass. Co-Authored-By: Claude Opus 5 --- docs/release_log.rst | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/release_log.rst b/docs/release_log.rst index cf51103a..c0e18565 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -23,11 +23,11 @@ Release Log **Breaking Changes** - **Add docs/design/ contributor documentation:** ``rules.md`` (the parser's normative rules, with executable examples), ``decisions.md`` (the decision record) and ``mechanisms.md`` (the solution-pattern catalog). New tests execute every documented example and verify every code citation - - **Change every vocabulary set in nameparser.config to a frozenset.** Editing one in place -- ``TITLES.add("dean")``, the old way of changing a global default -- now raises ``AttributeError`` at the line that writes it. It was never a reliable way to change a default: whether an edit reached a given parse depended on which config objects had already been built, so one program could hold two disagreeing defaults with nothing to say so. To change the defaults for ``HumanName``, build a private ``Constants`` and pass it (``c = Constants(); c.titles.add("dean"); HumanName(name, constants=c)``); for the 2.0 API, build a lexicon (``Parser(lexicon=Lexicon.default().add(titles={"dean"}))``). Mutating the shared ``CONSTANTS`` still works, but warns and goes away in 3.0. ``CAPITALIZATION_EXCEPTIONS`` is a mapping, not a set, and is unchanged. See :doc:`migrate` and :doc:`customize` (#293) + - **Change every vocabulary set in nameparser.config to a frozenset.** Editing one in place -- ``TITLES.add("dean")``, the old way of changing a global default -- now raises ``AttributeError`` at the line that writes it. To change the defaults for ``HumanName``, build a private ``Constants`` and pass it (``c = Constants(); c.titles.add("dean"); HumanName(name, constants=c)``); for the 2.0 API, build a lexicon (``Parser(lexicon=Lexicon.default().add(titles={"dean"}))``). Mutating the shared ``CONSTANTS`` still works, but warns and goes away in 3.0. ``CAPITALIZATION_EXCEPTIONS`` is a mapping, not a set, and is unchanged. See :doc:`migrate` and :doc:`customize` (#293) **Behavior Changes** - - **Fix a title changing how the name behind it is read.** ``"Dr. Van Johnson"`` gave family ``Van Johnson`` with no given name, and ``"Sir Van Johnson"`` gave given ``Van Johnson`` with no family at all; both now read given ``Van``, family ``Johnson`` -- the reading the untitled ``"Van Johnson"`` has always had. A leading word that is *both* a title and a particle is unchanged: ``"St John Smith"``, ``"Do John Smith"`` and ``"Freiherr von Richthofen"`` keep their readings. This fixes a defect v1 shipped as a known-failing test for the life of the 1.x line. See the ``P2`` entry of ``docs/design/decisions.md`` (closes #367) + - **Fix a title changing how the name behind it is read.** ``"Dr. Van Johnson"`` gave family ``Van Johnson`` with no given name, and ``"Sir Van Johnson"`` gave given ``Van Johnson`` with no family at all; both now read given ``Van``, family ``Johnson`` -- the reading the untitled ``"Van Johnson"`` has always had. A leading word that is *both* a title and a particle is unchanged: ``"St John Smith"``, ``"Do John Smith"`` and ``"Freiherr von Richthofen"`` keep their readings. See the ``P2`` entry of ``docs/design/decisions.md`` (closes #367) - **Fix a given-name title keeping a bound given name from joining the word after it.** ``"Sheik abdul salam"`` read given ``abdul``, family ``salam``, and now reads given ``abdul salam`` with an empty family, as ``"Sir John"`` does; ``"الشيخ عبد الله"`` reads given ``عبد الله``. A title that addresses by family is unchanged (``"Dr. abdul salam"``). This also restores ``"Sheik Abu Bakar"`` to given ``Abu Bakar``, which the fix above had regressed, and drops the ``PARTICLE_OR_GIVEN`` ambiguity that name reported through 2.1 (closes #369) @@ -37,17 +37,17 @@ Release Log - **Fix a bound given name joining past a credential that the suffix rule then takes, leaving no family.** ``"abdul Smith Jr Ma"`` read given ``abdul Smith`` with no family and now reads family ``Smith``, suffix ``Jr, Ma``, as ``"John Smith Jr Ma"`` does; ``"abdul Smith Ma"`` reads family ``Smith``, suffix ``Ma``. Both as 1.4.0 read them. ``"abdul Smith Berg Ma"`` keeps its join, and ``"Berg, abdul Sir"`` still reads given ``abdul Sir`` (closes #425) - - **Remove the Czech/Slovak abbreviation roz. from the default maiden markers, where it had been eating English given names.** Marker matching is case-folded and period-insensitive, so ``Roz`` -- the diminutive of Rosalind -- was the same string as the marker, and a marker takes every word after it: ``"Rosalind Roz Smith"`` read maiden ``Smith`` with **no family name at all**. It and ``"Rosalind Roz Jones Smith"`` now read as 1.4.0 read them. What stops working is the abbreviation itself, for Czech and Slovak callers: ``"Anna Nováková roz. Svobodová"`` reads middle ``Nováková roz.``, family ``Svobodová``, which is also 1.4.0's reading. The full participle is untouched -- ``"Anna Nováková rozená Svobodová"`` still reads maiden ``Svobodová`` -- and a caller who wants the abbreviation back adds it to their own lexicon: ``Parser(lexicon=Lexicon.default().add(maiden_markers={"roz"}))`` (found in #335's review) + - **Remove the Czech/Slovak abbreviation roz. from the default maiden markers.** Marker matching is case-folded and period-insensitive, so ``Roz`` -- the diminutive of Rosalind -- was the same string as the marker, and a marker takes every word after it: ``"Rosalind Roz Smith"`` read maiden ``Smith`` with **no family name at all**. It and ``"Rosalind Roz Jones Smith"`` now read as 1.4.0 read them. The full participle is untouched -- ``"Anna Nováková rozená Svobodová"`` still reads maiden ``Svobodová`` -- and a caller who wants the abbreviation back adds it to their own lexicon: ``Parser(lexicon=Lexicon.default().add(maiden_markers={"roz"}))`` (found in #335's review) - - **Add the Polish maiden marker z domu to the default vocabulary, and let a maiden_markers entry be more than one word.** ``"Maria Kowalska z domu Nowak"`` now reads family ``Kowalska``, maiden ``Nowak``, where every earlier version read the marker as part of the name (1.4.0: middle ``Kowalska z domu``, family ``Nowak``). The bracketed spelling moves with it, while a clause holding nothing but the marker stays a nickname (``"Maria Kowalska (z domu)"``). **If you worked around the old limit by storing z and domu as separate entries, remove both** -- a bare preposition is then a marker, and a marker takes every word after it, so ``"Anna z Nowak"`` loses its surname. ``maiden_markers`` and ``given_name_titles`` are now the two fields exempt from the multi-word warning. See :doc:`customize` (#434) + - **Add the Polish maiden marker z domu to the default vocabulary, and let a maiden_markers entry be more than one word.** ``"Maria Kowalska z domu Nowak"`` now reads family ``Kowalska``, maiden ``Nowak``, where every earlier version read the marker as part of the name (1.4.0: middle ``Kowalska z domu``, family ``Nowak``). The bracketed spelling moves with it. ``maiden_markers`` and ``given_name_titles`` are now the two fields exempt from the multi-word warning. See :doc:`customize` (#434) - - **Fix a bracketed maiden clause reading as a nickname because its brackets were not declared.** ``"Jane Smith nee Jones"`` gave maiden ``Jones`` while ``"Jane Smith (née Jones)"`` gave nickname ``née Jones``; the bracketed spelling now reads family ``Smith``, maiden ``Jones`` too, and so does the Japanese ``"山田 花子(旧姓 佐藤)"``, which needed ``Policy(maiden_delimiters=...)`` through 2.1. Every delimiter pair the parser ships moves the same way, quotes included. An interior clause no longer eats the name behind it (``"Jane (née Jones) Smith"`` keeps family ``Smith``), and two clauses beside each other each keep their own role (``"Jane "Janey" Smith (née Jones)"`` reads nickname ``Janey``, maiden ``Jones``). What does NOT move: a lone marker in brackets is just a word, so ``"Jane Smith (née)"`` stays a nickname, and a markerless clause stays a nickname too -- which is what ``Policy(maiden_delimiters=...)`` remains for. This reaches ``HumanName`` (closes #335) + - **Fix a bracketed maiden clause reading as a nickname because its brackets were not declared.** ``"Jane Smith nee Jones"`` gave maiden ``Jones`` while ``"Jane Smith (née Jones)"`` gave nickname ``née Jones``; the bracketed spelling now reads family ``Smith``, maiden ``Jones`` too, and so does the Japanese ``"山田 花子(旧姓 佐藤)"``, which needed ``Policy(maiden_delimiters=...)`` through 2.1. Every delimiter pair the parser ships moves the same way, quotes included. An interior clause no longer eats the name behind it (``"Jane (née Jones) Smith"`` keeps family ``Smith``), and two clauses beside each other each keep their own role (``"Jane "Janey" Smith (née Jones)"`` reads nickname ``Janey``, maiden ``Jones``). A clause with no marker in it is still a nickname, which is what ``Policy(maiden_delimiters=...)`` remains for. This reaches ``HumanName`` (closes #335) - **Fix a particle chain and a maiden name taking a trailing generational numeral as a name word.** ``"John van der Berg V"`` read family ``van der Berg V`` and ``"John née Jones Smith V"`` read maiden ``Jones Smith V``, where ``"John Smith V"`` reads suffix ``V``; both now stop before the numeral, for ``I`` and ``X`` alike. A word before the numeral that is an initial keeps its reading (``"John van der J. V"``). The chain also stops before a bare credential with words to spare -- ``"John van der Berg Ma"`` reads suffix ``Ma``, as 1.4.0 did -- and no longer swallows the given name behind an unlisted abbreviation: ``"Xyz. van Johnson"`` and ``"Esq. van Gogh"`` read given ``van`` (closes #424) - **Fix a name losing its given/family split when a comma is followed only by an honorific.** ``"John Smith, Mr."`` returned the whole of ``"John Smith"`` as the family name and now gives given ``John``, family ``Smith``, title ``Mr.``: it is ``"Mr. John Smith"`` with the honorific moved to the end, and marks no surname boundary. A comma followed by an actual name still fixes the family (``"John Smith, Jones"``), and a single pre-comma piece has no split to keep (``"Smith, Dr."`` is unchanged). The pre-comma name now also picks up the declared name order -- ``"de Mesnil Juan, Dr."`` keeps family ``de Mesnil`` under a family-first order -- and the particle-or-given ambiguity report (``"Van Johnson, Mr."``) - - **Fix pure postnominals being claimed as titles:** ``jr``, ``junior``, ``phd``, ``do`` and ``se`` have left the default ``titles`` vocabulary, and ``dr``/``sra`` have left the suffix vocabulary they never belonged in, so ``"Smith, PhD"`` gives suffix rather than title ``PhD``. All nineteen words in the ``titles``/suffix overlap were given a positional class; twelve are genuine duals and keep both memberships, with position deciding -- ``"Lt. Smith"`` is a title, ``"Smith, LT"`` a postnominal, bare ``Md`` before a name the Bengali and South Asian abbreviation of Muhammad, ``MD`` after it the degree. The cost is in leading position, where a dropped word now reads as a name: ``"PhD Smith"`` gives given ``PhD``, which is what makes ``"Do Nguyen"`` parse as the Vietnamese name it is. ``dr`` and ``sra`` also stop being recognized in trailing position, so ``"John Smith Dr."`` gives family ``Dr.`` as ``"John Smith Prof."`` always has. An ambiguous credential acronym (``ma``, ``ed``, ``jd``, ``do``) counts as a suffix only when written with its periods, so ``"Jack Ma."`` keeps family ``Ma.`` as 1.4.0 read it. Routing a trailing title word to ``title`` is a separate open question (#316) + - **Fix pure postnominals being claimed as titles:** ``jr``, ``junior``, ``phd``, ``do`` and ``se`` have left the default ``titles`` vocabulary, and ``dr``/``sra`` have left the suffix vocabulary they never belonged in, so ``"Smith, PhD"`` gives suffix rather than title ``PhD``. Twelve words are genuine duals and keep both memberships, with position deciding -- ``"Lt. Smith"`` is a title, ``"Smith, LT"`` a postnominal, bare ``Md`` before a name the Bengali and South Asian abbreviation of Muhammad, ``MD`` after it the degree. The cost is in leading position, where a dropped word now reads as a name: ``"PhD Smith"`` gives given ``PhD``, which is what makes ``"Do Nguyen"`` parse as the Vietnamese name it is. ``dr`` and ``sra`` also stop being recognized in trailing position, so ``"John Smith Dr."`` gives family ``Dr.``. An ambiguous credential acronym (``ma``, ``ed``, ``jd``, ``do``) counts as a suffix only when written with its periods, so ``"Jack Ma."`` keeps family ``Ma.`` as 1.4.0 read it. Routing a trailing title word to ``title`` is a separate open question (#316) - **Fix a credential run after a one-word family comma reading as a title or a given name.** ``"Smith, Jr."`` and ``"Smith, PhD"`` now give suffix ``Jr.``/``PhD`` where they gave title, and ``"Smith, Ph. D. Jr."`` gives suffix ``Ph. D. Jr.`` where the split credential landed in the given name -- a regression from 1.4.0. The position right after a family comma is postnominal position. Vocabulary still decides which words qualify (``"Smith, Dr."`` keeps title ``Dr.``), the leading readings are untouched (``"Sr. Garcia"`` is still title ``Sr.``), and a name word in the run makes it the given-and-suffix reading it always had (``"Smith, John Jr."``) (closes #296, closes #325) @@ -57,41 +57,41 @@ Release Log - **Fix a name opening with a particle that is never a given name being split at the particle under a family-first name order.** ``"de Mesnil"`` read as family ``de``, given ``Mesnil`` and ``"de la Vega"`` as family ``de``, given ``la Vega``; each is now the whole surname, as it has always been in the default order, under ``FAMILY_FIRST`` and ``FAMILY_FIRST_GIVEN_LAST`` alike. A word that can never be a given name leaves ``name_order`` nothing to decide. Standing alone is the whole of it: ``"Juan de la Vega"`` under ``FAMILY_FIRST`` still reports given ``de la Vega``. A leading particle that *may* be a given name is genuinely order-dependent and is untouched, so ``"van Gogh"`` still reads family ``van``, given ``Gogh`` under both family-first orders (closes #359) - - **Fix a family name made only of particle words reporting no base, so the surname vanished from family_base and from the initials.** ``parse("Anh Do")`` gave family ``Do`` with ``family_base`` ``''`` and initials ``A.``, and under ``Policy(name_order=FAMILY_FIRST)`` ``"Del Toro"`` gave family ``Del`` the same way. A particle standing alone in a name part is not doing a particle's work there and now reads as an ordinary name word: ``"Anh Do"`` is base ``Do``, initials ``A. D.``; ``"Juan van der"`` is base ``van der``, initials ``J. v. d.``; ``"Nguyen, Van Le"`` initials ``V. L. N.`` where the middle name used to be dropped. Where the particles DO join a name word nothing changes (``"Juan de la Vega"`` keeps base ``Vega``). The parse fields themselves do not move -- only the derived views and the initials. See the ``R2`` entry of ``docs/design/decisions.md`` (closes #385, closes #402) + - **Fix a family name made only of particle words reporting no base, so the surname vanished from family_base and from the initials.** ``parse("Anh Do")`` gave family ``Do`` with ``family_base`` ``''`` and initials ``A.``, and under ``Policy(name_order=FAMILY_FIRST)`` ``"Del Toro"`` gave family ``Del`` the same way. A particle standing alone in a name part is not doing a particle's work there and now reads as an ordinary name word: ``"Anh Do"`` is base ``Do``, initials ``A. D.``; ``"Juan van der"`` is base ``van der``, initials ``J. v. d.``; ``"Nguyen, Van Le"`` initials ``V. L. N.`` where the middle name used to be dropped. The parse fields themselves do not move -- only the derived views and the initials. See the ``R2`` entry of ``docs/design/decisions.md`` (closes #385, closes #402) - - **Fix case repair lowercasing the words of a family name made only of particle words, where every other view already reads them as ordinary name words.** ``HumanName("ANH DO").capitalize()`` gives ``Anh Do`` where it gave ``Anh do``, and ``"anh van do"`` gives ``Anh Van Do``. The test is the whole part, not a particle standing alone, which is why the two-word family moves along with the one-word one. This DIFFERS FROM 1.4.0 deliberately and does not restore it: 1.4.0 returned ``Anh do``. The accepted cost is that a family which is nothing but particles capitalizes too, so ``"juan van der"`` gives ``Juan Van Der``. A conjunction is untouched (``"der, y van"`` gives ``y Van Der``), and where the particles DO join a name word nothing changes (``"juan de la vega"`` still gives ``Juan de la Vega``) (closes #407) + - **Fix case repair lowercasing the words of a family name made only of particle words, where every other view already reads them as ordinary name words.** ``HumanName("ANH DO").capitalize()`` gives ``Anh Do`` where it gave ``Anh do``, and ``"anh van do"`` gives ``Anh Van Do``. This DIFFERS FROM 1.4.0 deliberately and does not restore it: 1.4.0 returned ``Anh do``. The accepted cost is that a family which is nothing but particles capitalizes too, so ``"juan van der"`` gives ``Juan Van Der``. A conjunction is untouched (``"der, y van"`` gives ``y Van Der``), and where the particles DO join a name word nothing changes (``"juan de la vega"`` still gives ``Juan de la Vega``) (closes #407) - - **Change case repair to read the parser's own conjunction tag instead of re-deciding, from the word's spelling, whether a word is a conjunction or an initial.** Two spellings of one name disagreed because of it: ``"juan e-f smith"`` capitalized to ``Juan e-F Smith`` while ``"JUAN E-F SMITH"`` gave ``Juan E-F Smith``; both give ``Juan E-F Smith`` now. A conjunction written as a word of its own is untouched (``"juan y garcia"`` still repairs to ``Juan y Garcia``). A field assigned after the parse was never classified, so repair asks the vocabulary there -- today's vocabulary, which is narrower than 1.4.0 parity: ``h.last = "хосе и мария сантос"`` gives ``Хосе И Мария Сантос`` on 1.4.0 and ``Хосе и Мария Сантос`` here. One reading changes for hand-built ``Token``\ s in the 2.0 API: an untagged token whose text is conjunction vocabulary now capitalizes as an ordinary name word. See the ``R4`` entry of ``docs/design/decisions.md`` (closes #458) + - **Change case repair to read the parser's own conjunction tag instead of re-deciding, from the word's spelling, whether a word is a conjunction or an initial.** Two spellings of one name disagreed because of it: ``"juan e-f smith"`` capitalized to ``Juan e-F Smith`` while ``"JUAN E-F SMITH"`` gave ``Juan E-F Smith``; both give ``Juan E-F Smith`` now. A field assigned after the parse was never classified, so repair asks the vocabulary there -- today's vocabulary, which is narrower than 1.4.0 parity: ``h.last = "хосе и мария сантос"`` gives ``Хосе И Мария Сантос`` on 1.4.0 and ``Хосе и Мария Сантос`` here. One reading changes for hand-built ``Token``\ s in the 2.0 API: an untagged token whose text is conjunction vocabulary now capitalizes as an ordinary name word. See the ``R4`` entry of ``docs/design/decisions.md`` (closes #458) - - **Change the parse-cost benchmark to bound function calls per parse rather than wall-clock seconds.** The two ``under_a_second`` tests asserted that 1000 parses take under a second; on CI that bound failed four times across two branches at 1.01 to 1.08 seconds while the same code re-ran green on master. Frame counts do not move under load, so growth shows up in a diff instead of in a threshold that happens to break. The bound is a per-interpreter band of ±2%, with a loose five-second backstop over both entry points for the class frame counts cannot see. Every figure is recomputable with ``uv run python tools/perf/call_count.py --against v2.1.0``; the counts and the per-PR attribution are in the ``parse-cost`` entry of ``docs/design/decisions.md``. No user-visible behavior changes (closes #475) + - **Change the parse-cost benchmark to bound function calls per parse rather than wall-clock seconds.** The old one-second bound failed four times on CI while the same code re-ran green on master; frame counts do not move under load. The bound is a per-interpreter band of ±2%, with a loose five-second backstop for what frame counts cannot see. Recomputable with ``uv run python tools/perf/call_count.py --against v2.1.0``; the counts and the per-PR attribution are in the ``parse-cost`` entry of ``docs/design/decisions.md``. No user-visible behavior changes (closes #475) - - **Fix a name that opens with a spaced Ph. D. losing its surname.** ``parse("Ph. D. Van Johnson")`` read given ``Van Johnson`` with an empty ``family`` and suffix ``Ph. D.``; it now reads title ``Ph.``, given ``D.``, family ``Van Johnson``. A suffix never begins a name, and every other suffix-shaped word standing first already fell out as a title (``Jr.``, ``MD``, ``Esq.``) or as an ordinary name word (``PhD``, ``III``), so the split credential was the only shape that reached the defect. A family comma still opens a listing rather than a name (``"John Smith Ph. D."`` and ``"Smith, Ph. D. Jr."`` keep their suffixes), and "the head" means the head of the string rather than of the name, so ``"Sir Ph. D. Van Johnson"`` is unchanged. This RESTORES 1.4.0. One accepted consequence: ``Parser.revise(suffix="Ph. D.")`` renders ``Ph., D.`` (closes #371) + - **Fix a name that opens with a spaced Ph. D. losing its surname.** ``parse("Ph. D. Van Johnson")`` read given ``Van Johnson`` with an empty ``family`` and suffix ``Ph. D.``; it now reads title ``Ph.``, given ``D.``, family ``Van Johnson``. A suffix never begins a name, and the split credential was the only shape that reached the defect. A family comma still opens a listing rather than a name (``"John Smith Ph. D."`` and ``"Smith, Ph. D. Jr."`` keep their suffixes), and "the head" means the head of the string rather than of the name, so ``"Sir Ph. D. Van Johnson"`` is unchanged. This RESTORES 1.4.0. One accepted consequence: ``Parser.revise(suffix="Ph. D.")`` renders ``Ph., D.`` (closes #371) - - **Fix a trailing surname particle being stranded as a standalone middle name under a family-first name order.** Under ``Policy(name_order=FAMILY_FIRST)`` the same listing written with a comma reads it as part of the surname: ``"Jong Anke de"`` gave family ``Jong`` with ``de`` left as a middle name and now gives family ``de Jong``, given ``Anke`` -- the answer ``parse("Jong, Anke de")`` has always given. The test is the SLOT the particle landed in, not the word, and ``FAMILY_FIRST`` is the only order that puts a trailing piece in a middle; ``FAMILY_FIRST_GIVEN_LAST`` puts it in the given slot, so ``"Nguyen Thi Van"`` under that order still reads given ``Van``. That reads both traditions without asking about the vocabulary: ``"Beethoven Ludwig van"`` under ``FAMILY_FIRST`` now gives family ``van Beethoven``. A particle standing alone in the given slot is no longer folded into the family either, so ``"Ménil de"`` reports given ``de``. Nothing moves under the DEFAULT name order. See the ``P6`` entry of ``docs/design/decisions.md`` (closes #467) + - **Fix a trailing surname particle being stranded as a standalone middle name under a family-first name order.** Under ``Policy(name_order=FAMILY_FIRST)`` the same listing written with a comma reads it as part of the surname: ``"Jong Anke de"`` gave family ``Jong`` with ``de`` left as a middle name and now gives family ``de Jong``, given ``Anke`` -- the answer ``parse("Jong, Anke de")`` has always given. ``FAMILY_FIRST`` is the only order that puts a trailing piece in a middle; ``FAMILY_FIRST_GIVEN_LAST`` puts it in the given slot, so ``"Nguyen Thi Van"`` under that order still reads given ``Van``. ``"Beethoven Ludwig van"`` under ``FAMILY_FIRST`` now gives family ``van Beethoven``. A particle standing alone in the given slot is no longer folded into the family either, so ``"Ménil de"`` reports given ``de``. Nothing moves under the DEFAULT name order. See the ``P6`` entry of ``docs/design/decisions.md`` (closes #467) - **Fix initials() ordering a name differently from the fields of the same parse.** Two rules fold words into the family and render them ahead of it -- ``Policy(middle_as_family=True)`` and the tussenvoegsel attachment after a family comma -- and the ``family`` field honored the fold where ``initials()`` did not: ``parse("der, y van")`` gave family ``van der`` but initials ``y. d. v.``, and now gives ``y. v. d.``. Under ``middle_as_family`` this RESTORES v1, that option being ``middle_name_as_last``'s successor: ``"Doe, Dr. John A."`` gives ``J. A. D.`` again where 2.0 through 2.2 gave ``J. D. A.``. ``HumanName.initials()`` was already right and is unchanged. See the ``R3`` entry of ``docs/design/decisions.md`` (closes #408) - - **Fix a tussenvoegsel attached to the family name after a comma deciding a genuinely uncertain reading and reporting nothing.** ``"Van Johnson"`` reports a ``PARTICLE_OR_GIVEN`` ambiguity -- ``Van`` is a Dutch particle and a Vietnamese given name -- while ``"Nguyen, Thi Van"`` picked the same word the same way, silently. The attachment now reports the fork it decides: ``"Nguyen, Thi Van"``, ``"Berg, Jan van der"`` and ``"Vega, Juan de la"`` each gain a ``PARTICLE_OR_GIVEN``, while a particle already read as a post-nominal reports ``SUFFIX_OR_NAME`` instead (``"Berg, Jan vd"``). A particle where nothing was overridden reports nothing, so ``"Jong, Piet de"`` is unchanged. Worth knowing before you filter on this: ``"Beethoven, Ludwig van"`` -- read exactly right -- now carries a report too, nothing in the input separating it from ``"Nguyen, Thi Van"``. ``ambiguities`` is the only value that grows (closes #405) + - **Fix a tussenvoegsel attached to the family name after a comma deciding a genuinely uncertain reading and reporting nothing.** ``"Van Johnson"`` reports a ``PARTICLE_OR_GIVEN`` ambiguity -- ``Van`` is a Dutch particle and a Vietnamese given name -- while ``"Nguyen, Thi Van"`` picked the same word the same way, silently. The attachment now reports the fork it decides: ``"Nguyen, Thi Van"``, ``"Berg, Jan van der"`` and ``"Vega, Juan de la"`` each gain a ``PARTICLE_OR_GIVEN``, while a particle already read as a post-nominal reports ``SUFFIX_OR_NAME`` instead (``"Berg, Jan vd"``). Worth knowing before you filter on this: ``"Beethoven, Ludwig van"`` -- read exactly right -- now carries a report too, nothing in the input separating it from ``"Nguyen, Thi Van"``. ``ambiguities`` is the only value that grows (closes #405) - - **Fix a tussenvoegsel after a family comma being parsed as a middle name.** Dutch and Belgian alphabetized listings move the particle behind the given name -- ``"Beethoven, Ludwig van"`` is how ``"Ludwig van Beethoven"`` is filed -- and it was read as a middle name rather than as part of the surname: ``"Beethoven, Ludwig van"`` gave middle ``van``, last ``Beethoven``, and ``"Berg, Jan van der"`` gave middle ``van der``. Those now read family ``van Beethoven`` and ``van der Berg``, and the derived views follow, so ``family_particles`` is ``van`` and ``family_base`` ``Beethoven``. Two guards bound it: a name whose only given word is the particle keeps it (``"Nguyen, Van"`` still reads given ``Van``), and where the word is BOTH particle and suffix vocabulary the attachment wins, so ``"Berg, Jan vd"`` reads family ``vd Berg`` where 1.4.0 and 2.1 alike gave suffix ``vd`` -- as does ``mc``. Names without the comma are untouched (closes #379, closes #380) + - **Fix a tussenvoegsel after a family comma being parsed as a middle name.** Dutch and Belgian alphabetized listings move the particle behind the given name -- ``"Beethoven, Ludwig van"`` is how ``"Ludwig van Beethoven"`` is filed -- and it was read as a middle name rather than as part of the surname: ``"Beethoven, Ludwig van"`` gave middle ``van``, last ``Beethoven``, and ``"Berg, Jan van der"`` gave middle ``van der``. Those now read family ``van Beethoven`` and ``van der Berg``. Two guards bound it: a name whose only given word is the particle keeps it (``"Nguyen, Van"`` still reads given ``Van``), and where the word is BOTH particle and suffix vocabulary the attachment wins, so ``"Berg, Jan vd"`` reads family ``vd Berg`` where 1.4.0 and 2.1 alike gave suffix ``vd`` -- as does ``mc``. (closes #379, closes #380) - **Add abd to BOUND_GIVEN_NAMES, so the spellings that write the article as its own word join like the others do:** ``"abd Allah Smith"`` was given ``abd``, middle ``Allah`` and is now given ``abd Allah``. ``abdul``, ``abdel`` and ``abdal`` were already there, and the Arabic-script ``عبد`` has covered the same word since 2.0, so only the Latin spelling was short. The word is also the postnominal ABD ("All But Dissertation") and stays in ``SUFFIX_ACRONYMS``: position tells the two readings apart, so ``"Jane Smith ABD"``, ``"Jane Smith, ABD"`` and ``"Jane Smith A.B.D."`` all still read the credential as a suffix (#400) - - **Change how far a leading never-given particle takes the surname when a family-first name_order is declared.** ``Policy(name_order=FAMILY_FIRST)`` read ``"de Mesnil Juan"`` as family ``de Mesnil Juan`` -- the whole name -- and now reads family ``de Mesnil``, given ``Juan``. The default order is unchanged, deliberately: with no order declared nothing marks where the surname ends, and a particle followed by several words really can be all surname (``von Bergen Wessels``); a caller who means family ``de la Vega`` plus given ``Juan`` there writes the comma. The run takes one name WORD rather than one token, so the stop cannot land inside a conjunction-joined run or a bound given-name pair: ``"de la Vega y Santos Juan"`` reads family ``de la Vega y Santos``, ``"ibn Awf abdul Rahman"`` given ``abdul Rahman``. Where two or more words are left over, the two family-first orders differ from each other for the first time: ``"de la Cruz Juan Carlos"`` reads given ``Juan``, middle ``Carlos`` under ``FAMILY_FIRST`` and the reverse under ``FAMILY_FIRST_GIVEN_LAST``. An ambiguous leading particle and a family comma are both untouched. See the ``P1`` entry of ``docs/design/decisions.md`` (closes #395) + - **Change how far a leading never-given particle takes the surname when a family-first name_order is declared.** ``Policy(name_order=FAMILY_FIRST)`` read ``"de Mesnil Juan"`` as family ``de Mesnil Juan`` -- the whole name -- and now reads family ``de Mesnil``, given ``Juan``. The default order is unchanged, deliberately: with no order declared nothing marks where the surname ends, and a particle followed by several words really can be all surname (``von Bergen Wessels``); a caller who means family ``de la Vega`` plus given ``Juan`` there writes the comma. The stop cannot land inside a conjunction-joined run or a bound given-name pair: ``"de la Vega y Santos Juan"`` reads family ``de la Vega y Santos``, ``"ibn Awf abdul Rahman"`` given ``abdul Rahman``. Where two or more words are left over, the two family-first orders differ from each other for the first time: ``"de la Cruz Juan Carlos"`` reads given ``Juan``, middle ``Carlos`` under ``FAMILY_FIRST`` and the reverse under ``FAMILY_FIRST_GIVEN_LAST``. See the ``P1`` entry of ``docs/design/decisions.md`` (closes #395) - **Change the detail text of a PARTICLE_OR_GIVEN ambiguity to name the role the leading particle was actually given.** It said "read as a given name" under every ``name_order``, which is false under ``Policy(name_order=FAMILY_FIRST)`` -- there ``"Van Johnson"`` reads family ``Van``, given ``Johnson``, and the report described the reading not taken. It now ends "read as a family name" in that case. The ``kind`` is unchanged and stays ``PARTICLE_OR_GIVEN``; only the human-readable text moved, and default-order output is identical (#355) - - **Fix a maiden name being lost when a particle stood in front of the marker.** ``"Ursula Leyen geb. Albrecht"`` reported maiden ``Albrecht`` correctly, but ``"Ursula von der Leyen geb. Albrecht"`` -- the same words one particle chain apart -- gave family ``von der Leyen geb. Albrecht`` and no maiden name at all, as did ``"Jane van der Berg née Jones"``. A suffix already stopped the particle chain; a marker now does too, so those read family ``von der Leyen`` maiden ``Albrecht`` and family ``van der Berg`` maiden ``Jones``. A single leading particle always worked (``"von Müller geb. Schmidt"``); a leading run of two did not. A marker needs a word after it to hand the maiden name to, so a trailing one bounds nothing and ``"Jane van der Berg née"`` keeps family ``van der Berg née`` -- which is what keeps ``"Jane van der Nee"`` intact, ``Nee`` being an attested surname as well as a marker spelling. Under a family-first order ``"de la Cruz née Vega"`` now reads family ``de la Cruz``, maiden ``Vega``. Two limits remain, both recorded in ``rules.md#M2``: a conjunction join and a bound given-name join each still absorb a marker first (closes #399) + - **Fix a maiden name being lost when a particle stood in front of the marker.** ``"Ursula Leyen geb. Albrecht"`` reported maiden ``Albrecht`` correctly, but ``"Ursula von der Leyen geb. Albrecht"`` -- the same words one particle chain apart -- gave family ``von der Leyen geb. Albrecht`` and no maiden name at all, as did ``"Jane van der Berg née Jones"``. A suffix already stopped the particle chain; a marker now does too, so those read family ``von der Leyen`` maiden ``Albrecht`` and family ``van der Berg`` maiden ``Jones``. Under a family-first order ``"de la Cruz née Vega"`` now reads family ``de la Cruz``, maiden ``Vega``. Two limits remain, both recorded in ``rules.md#M2``: a conjunction join and a bound given-name join each still absorb a marker first (closes #399) - - **Move mc and ste into the never-given half of the particle vocabulary, and add los, las and das.** The Spanish and Portuguese articles 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"`` becomes last ``Mc Donald Smith``. The ``PARTICLE_OR_GIVEN`` ambiguity goes with it for ``mc`` and ``ste``, which only changed halves -- ``"Jack Mc Donald"`` 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"`` is now last ``das Neves``. Never-given membership goes from 28 words to 33 (closes #360) + - **Move mc and ste into the never-given half of the particle vocabulary, and add los, las and das.** The Spanish and Portuguese articles 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"`` becomes last ``Mc Donald Smith``. The ``PARTICLE_OR_GIVEN`` ambiguity goes with it for ``mc`` and ``ste``. ``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"`` is now last ``das Neves``. (closes #360) - - **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 reads given ``abdul``, family ``Berg``; it now reads given ``abdul``, family ``Berg``, maiden ``Jones``. The join also declines when the piece it would absorb is a marker, so ``"van der Berg, abdul née Jones"`` reads given ``abdul``, family ``van der Berg``, maiden ``Jones`` where it read given ``abdul née``. A name with a word to spare is untouched (``"abd Allah Smith née Jones"`` still joins). Where the bound word is ALSO suffix vocabulary, a declining join after a family comma leaves the post-nominal reading: ``"Berg, abd née Jones"`` reads family ``Berg``, suffix ``abd``, maiden ``Jones``, as ``"Berg, abd"`` alone always has (closes #411) + - **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 reads given ``abdul``, family ``Berg``; it now reads given ``abdul``, family ``Berg``, maiden ``Jones``. The join also declines when the piece it would absorb is a marker, so ``"van der Berg, abdul née Jones"`` reads given ``abdul``, family ``van der Berg``, maiden ``Jones`` where it read given ``abdul née``. Where the bound word is ALSO suffix vocabulary, a declining join after a family comma leaves the post-nominal reading: ``"Berg, abd née Jones"`` reads family ``Berg``, suffix ``abd``, maiden ``Jones``, as ``"Berg, abd"`` alone always has (closes #411) - - **Fix a maiden clause changing how the rest of the name is read, and a connective join keeping the marker in the surname.** A maiden marker and the name it introduces are not part of the name they follow, but the grouping rules that count a name's words counted them anyway: ``"juan y garcia"`` reads given ``juan``, middle ``y``, family ``garcia``, while ``"juan y garcia nee jones"`` read given ``juan y garcia`` with NO family name at all -- eight more corpus names lose the family the same way under an appended clause. A name of two or more name words now reads as it reads without its maiden clause, plus the maiden name. The connective join used to merge the marker into a multi-word piece, so ``"Jane van der Berg née y Jones"`` kept family ``van der Berg née y Jones`` and now reads family ``van der Berg``, maiden ``y Jones``. Two limits: a bound given-name word still never joins onto a marker standing as a word of its own (``"Berg, abdul née PhD"``), and a suffix-vocabulary word inside the maiden name stops the marker, so ``"Jane née Jr y Jones"`` now reads family ``Jr y Jones`` with no maiden name (closes #412, closes #417, closes #418) + - **Fix a maiden clause changing how the rest of the name is read, and a connective join keeping the marker in the surname.** The grouping rules that count a name's words counted the maiden clause too: ``"juan y garcia"`` reads given ``juan``, middle ``y``, family ``garcia``, while ``"juan y garcia nee jones"`` read given ``juan y garcia`` with NO family name at all. A name of two or more name words now reads as it reads without its maiden clause, plus the maiden name. The connective join used to merge the marker into a multi-word piece, so ``"Jane van der Berg née y Jones"`` kept family ``van der Berg née y Jones`` and now reads family ``van der Berg``, maiden ``y Jones``. Two limits: a bound given-name word still never joins onto a marker standing as a word of its own (``"Berg, abdul née PhD"``), and a suffix-vocabulary word inside the maiden name stops the marker, so ``"Jane née Jr y Jones"`` now reads family ``Jr y Jones`` with no maiden name (closes #412, closes #417, closes #418) - **Fix a title-plus-surname name losing its family name whenever anything stood beside it.** ``"Dr. Smith"`` reads family ``Smith``, but ``"Dr. Smith née Jones"`` read given ``Smith`` with no family at all, and so did ``"Dr. Smith PhD"`` and ``"Dr. "Smitty" Smith"``. A suffix, a nickname and a maiden name each stand beside the name rather than in it, and the rule now counts name words alone: those read family ``Smith`` with maiden ``Jones``, suffix ``PhD`` and nickname ``Smitty`` respectively, and ``"Freiherr von Richthofen geb. Albrecht"`` reads family ``von Richthofen``, maiden ``Albrecht``. A given-name title still names no family: ``"Sir John née Jones"`` keeps given ``John``, exactly as ``"Sir John"`` does. One name moves where the nickname LEADS: ``"'Smitty' Dr. Jones"`` reads family ``Jones`` where it read given (closes #410) - - **Fix a name that is a surname and a maiden clause reporting no family at all.** ``"Smith née Jones"`` read given ``Smith`` with an empty ``family``; it now reads family ``Smith``, maiden ``Jones``. A maiden marker announces a FORMER surname, and a former surname only means something beside a current one, so the single name word left standing is the surname the bearer uses now. Every spelling of the shape moves, including a bracket pair you declared to mean maiden: under ``Policy(maiden_delimiters=frozenset({("(", ")")}))``, ``"Smith (Jones)"`` reads family ``Smith``, maiden ``Jones``. ``"Smith (née Jones)"`` RESTORES 1.4.0, which read family ``Smith`` with the clause as a nickname. An annotation beside the name is no part of it, so ``"Smith née Jones PhD"`` reads family ``Smith``, suffix ``PhD``. Two shapes deliberately do NOT move: a word the vocabulary claims as a given name (``"abd née Jones"``) and a word written as an initial (``"J. née Jones Smith V"``) -- this rule changes what POSITION decided and does not reach what a word already is. **If you have code that reads the lone name word beside a maiden clause out of given, this is the release where it moves to family** (closes #445) + - **Fix a name that is a surname and a maiden clause reporting no family at all.** ``"Smith née Jones"`` read given ``Smith`` with an empty ``family``; it now reads family ``Smith``, maiden ``Jones``. A maiden marker announces a FORMER surname, which only means something beside a current one, so the lone name word left standing is the surname in use now. Every spelling of the shape moves, including a bracket pair you declared to mean maiden: under ``Policy(maiden_delimiters=frozenset({("(", ")")}))``, ``"Smith (Jones)"`` reads family ``Smith``, maiden ``Jones``. ``"Smith (née Jones)"`` RESTORES 1.4.0, which read family ``Smith`` with the clause as a nickname. Two shapes deliberately do NOT move: a word the vocabulary claims as a given name (``"abd née Jones"``) and a word written as an initial (``"J. née Jones Smith V"``) -- this rule changes what POSITION decided and does not reach what a word already is. **If you have code that reads the lone name word beside a maiden clause out of given, this is the release where it moves to family** (closes #445) - **Change what a star import of the two 2.2 vocabulary modules binds.** ``nameparser.config.particles`` and ``nameparser.config.bound_given_names`` now declare ``__all__``, which their 1.x shims already did, so ``from ... import *`` binds their vocabulary alone -- it also bound the ``assert_normalized`` invariant helper, and from ``particles`` the ``BOUND_GIVEN_NAMES`` it imports only for a disjointness check. Importing a constant by name is unaffected, and no parse changes (#356) @@ -381,7 +381,7 @@ Release Log **Formatting and output fixes** - - Fix ``IndexError`` in ``initials()``/``initials_list()`` when a ``*_list`` attribute was assigned directly with an element containing unnormalized whitespace (e.g. ``name.middle_list = ['Q R']``), bypassing the parser's whitespace normalization (closes #232) + - Fix ``IndexError`` in ``initials()``/``initials_list()`` when a ``*_list`` attribute was assigned directly with an element containing unnormalized whitespace (e.g. ``name.middle_list = ['Q R']``), bypassing the parser's whitespace normalization (closes #232) - **Fix initials() emitting a stray empty initial (e.g. "J. . V.")** -- or raising ``TypeError`` when ``empty_attribute_default`` is ``None`` -- for name parts with no initialable words, e.g. a prefix-only middle name like ``"de la"`` - Fix capitalization of suffix acronyms written with dots, e.g. ``"M.D."`` (closes #141) - Fix extra whitespace before punctuation in ``str()`` output when a ``string_format`` field is empty (closes #139) From c4924d44cbcf6381a442499ae970773b9bc39a09 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Mon, 31 Aug 2026 20:46:11 -0700 Subject: [PATCH 09/10] docs(release_log,tests): a real name for the #458 example `juan e-f smith` was a constructed shape -- `e-f` is nobody's name. It is now `Ortega-y-Gasset`, which is a hyphenated last name (Derek's ask), a real one, and routinely hyphenated in library catalogues. Measured on the pre-#458 tree (git archive of 805bcf8^ into a scratch checkout, so nothing in this worktree moved): 'jose ortega-y-gasset' -> 'Jose Ortega-y-Gasset' 'JOSE ORTEGA-Y-GASSET' -> 'Jose Ortega-Y-Gasset' and both give 'Jose Ortega-Y-Gasset' here -- the same disagreement `e-f` showed, on a name someone actually has. WHY IT HAS TO BE THIS KIND OF HYPHENATED NAME, which is worth stating because the obvious simplification does not work: an ORDINARY hyphenated surname cannot witness this at all. Measured across both trees, 'bonham-carter', 'lloyd-webber', 'day-lewis', 'marie-smith' and 'jean-luc' are byte-identical before and after, in either case. The defect was case repair re-deciding conjunction-or-initial per word of a token's text, so the token has to CONTAIN a conjunction for there to be anything to get wrong. `y` is what makes the example work. The spaced form is now named in the bullet as the contrast, because a reader seeing 'Ortega-Y-Gasset' will ask: 'jose ortega y gasset' still repairs to 'Jose Ortega y Gasset', the conventional Spanish rendering, since there `y` is a token of its own and IS the conjunction. The claim is gate-invisible (decisions.md#R4 -- compare.py never calls capitalized()), so it is pinned by a unit test rather than left to prose. Mutation-checked: removing the UNCLASSIFIED_TAG gate in _render.py, which is the pre-#458 behaviour, fails both the new test and the existing `e-f` one. Co-Authored-By: Claude Opus 5 --- docs/release_log.rst | 2 +- tests/test_capitalization.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/release_log.rst b/docs/release_log.rst index c0e18565..6ad7ca56 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -61,7 +61,7 @@ Release Log - **Fix case repair lowercasing the words of a family name made only of particle words, where every other view already reads them as ordinary name words.** ``HumanName("ANH DO").capitalize()`` gives ``Anh Do`` where it gave ``Anh do``, and ``"anh van do"`` gives ``Anh Van Do``. This DIFFERS FROM 1.4.0 deliberately and does not restore it: 1.4.0 returned ``Anh do``. The accepted cost is that a family which is nothing but particles capitalizes too, so ``"juan van der"`` gives ``Juan Van Der``. A conjunction is untouched (``"der, y van"`` gives ``y Van Der``), and where the particles DO join a name word nothing changes (``"juan de la vega"`` still gives ``Juan de la Vega``) (closes #407) - - **Change case repair to read the parser's own conjunction tag instead of re-deciding, from the word's spelling, whether a word is a conjunction or an initial.** Two spellings of one name disagreed because of it: ``"juan e-f smith"`` capitalized to ``Juan e-F Smith`` while ``"JUAN E-F SMITH"`` gave ``Juan E-F Smith``; both give ``Juan E-F Smith`` now. A field assigned after the parse was never classified, so repair asks the vocabulary there -- today's vocabulary, which is narrower than 1.4.0 parity: ``h.last = "хосе и мария сантос"`` gives ``Хосе И Мария Сантос`` on 1.4.0 and ``Хосе и Мария Сантос`` here. One reading changes for hand-built ``Token``\ s in the 2.0 API: an untagged token whose text is conjunction vocabulary now capitalizes as an ordinary name word. See the ``R4`` entry of ``docs/design/decisions.md`` (closes #458) + - **Change case repair to read the parser's own conjunction tag instead of re-deciding, from the word's spelling, whether a word is a conjunction or an initial.** Two spellings of one name disagreed because of it: ``"jose ortega-y-gasset"`` capitalized to ``Jose Ortega-y-Gasset`` while ``"JOSE ORTEGA-Y-GASSET"`` gave ``Jose Ortega-Y-Gasset``; both give ``Jose Ortega-Y-Gasset`` now, a hyphenated token being one word to the parse whatever it contains. The spaced spelling is untouched and still repairs to ``Jose Ortega y Gasset``. A field assigned after the parse was never classified, so repair asks the vocabulary there -- today's vocabulary, which is narrower than 1.4.0 parity: ``h.last = "хосе и мария сантос"`` gives ``Хосе И Мария Сантос`` on 1.4.0 and ``Хосе и Мария Сантос`` here. One reading changes for hand-built ``Token``\ s in the 2.0 API: an untagged token whose text is conjunction vocabulary now capitalizes as an ordinary name word. See the ``R4`` entry of ``docs/design/decisions.md`` (closes #458) - **Change the parse-cost benchmark to bound function calls per parse rather than wall-clock seconds.** The old one-second bound failed four times on CI while the same code re-ran green on master; frame counts do not move under load. The bound is a per-interpreter band of ±2%, with a loose five-second backstop for what frame counts cannot see. Recomputable with ``uv run python tools/perf/call_count.py --against v2.1.0``; the counts and the per-PR attribution are in the ``parse-cost`` entry of ``docs/design/decisions.md``. No user-visible behavior changes (closes #475) diff --git a/tests/test_capitalization.py b/tests/test_capitalization.py index 22d93d5e..bdea44d7 100644 --- a/tests/test_capitalization.py +++ b/tests/test_capitalization.py @@ -310,6 +310,26 @@ def test_a_conjunction_inside_a_longer_token_is_a_name_word(self) -> None: uppered.capitalize() self.m(str(uppered), 'Juan E-F Smith', uppered) + # The same shape on a real name, which is what the release note + # cites: Ortega y Gasset is routinely hyphenated in catalogues, and + # `y` is conjunction vocabulary. Before #458 the two spellings + # repaired to 'Jose Ortega-y-Gasset' and 'Jose Ortega-Y-Gasset' + # (measured on the pre-#458 tree). The SPACED form is the contrast + # and is untouched -- there `y` is a token of its own and IS the + # conjunction, so it keeps the lowercase Spanish convention. + def test_a_hyphenated_compound_surname_capitalizes_its_conjunction( + self, + ) -> None: + lowered = HumanName('jose ortega-y-gasset') + lowered.capitalize(force=True) + self.m(str(lowered), 'Jose Ortega-Y-Gasset', lowered) + uppered = HumanName('JOSE ORTEGA-Y-GASSET') + uppered.capitalize() + self.m(str(uppered), 'Jose Ortega-Y-Gasset', uppered) + spaced = HumanName('jose ortega y gasset') + spaced.capitalize(force=True) + self.m(str(spaced), 'Jose Ortega y Gasset', spaced) + # The third producer of never-classified text, and the one that is # not an assignment: __getstate__ pickles the *_list STRINGS and # nothing else (mechanisms.md#FACADE-CONTRACT -- components come From fe1bccb0620977fddf5314417c098c7acf0b2a7a Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Mon, 31 Aug 2026 21:08:15 -0700 Subject: [PATCH 10/10] docs(release_log): say what the release is, plainly Derek on the old opener: "finishes the 2.0 rename at the layer it never reached" -- which layer, and why not just say so? The next sentence answered the riddle, so the riddle bought nothing. And a Lexicon that "has spoken of" particles is odd personification; it has USED them. Rewritten to lead with what the release IS, and the clusters are measured rather than asserted. Across the 37 bullets: surname particles 11, maiden-name clauses 7, Arabic bound given names 6, credentials after a comma 6, the rename 3, render views 2, tooling and docs 2. Behavior Changes holds 34 bullets, of which #475 and #356 are not parsing fixes, so "about thirty" is 32. Two things the count corrected in the framing we started from: name_order is the primary driver of FOUR bullets (#359, #467, #395, #355), not of most of the release -- but all four sit inside the particle thread, which IS the largest, so the story holds even though the mechanism is narrower than it felt. And name_order is 2.0's, not 2.1's; 2.1 added script_orders, the East-Asian script-driven one. What is new here is what DECLARING it is evidence for on Latin-script names, which is the one thing in the release a reader cannot reconstruct from the bullets, so it is the sentence that stayed. The constants are now named in the text (PREFIXES, BOUND_FIRST_NAMES, FIRST_NAME_TITLES) instead of the layer being alluded to, and the gate-is-the-source-of-record sentence went with the rest of the fluff -- each bullet already cites the issue that carries its measurement. Co-Authored-By: Claude Opus 5 --- docs/release_log.rst | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/release_log.rst b/docs/release_log.rst index 6ad7ca56..4544839c 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -2,20 +2,21 @@ Release Log =========== * 2.2.0 - Unreleased - nameparser 2.2 finishes the 2.0 rename at the layer it never - reached. The word lists in ``nameparser.config`` were still named - for v1's fields — prefixes, first names — while the ``Lexicon`` - they feed has spoken of particles and given names since 2.0. They - now agree, and the lists are frozen, which retires editing one in - place as a way to change a default. - - Nothing moved between vocabularies, and the rename itself changes - no parse. The parsing changes below are separate fixes. Most reach - the default name order; a bullet says so where its change is - family-first only or default-order only. Each names the shapes it - moves, the issue it closes carries the full measurement, and the - differential gate's output is the source of record for how many - names move. + nameparser 2.2 is a rename plus about thirty parsing fixes. + + The ``nameparser.config`` word lists were still named for v1's + fields — ``PREFIXES``, ``BOUND_FIRST_NAMES``, ``FIRST_NAME_TITLES`` + — while the ``Lexicon`` they feed has used particles and given + names since 2.0. They now agree, and the lists are frozen, which + retires editing one in place as a way to change a default. The + rename itself changes no parse. + + The fixes cluster around surname particles, largely what a declared + ``name_order`` means for Latin-script names, which this release + settles; then maiden-name clauses, Arabic bound given names, and + credentials after a comma. Most reach the default name order, and a + bullet says so where its change is family-first only. Each names the + shapes it moves, and the issue it closes carries the measurement. What breaks is code that *writes* to a default word list. Code that imports one by its 1.x name has until 3.0.