From 00dd27592671da4f2143e795ab01e787af786ec5 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sat, 17 Oct 2026 17:34:39 -0700 Subject: [PATCH 1/5] Add the elided particle and the hyphen-attached Arabic article (#700) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Library catalogs file "Paul d'Ivoi" as "Ivoi, Paul d'" and "Ḥusnī az-Zaʿīm" as "Zaʿīm, Ḥusnī az-": the particle written apart, behind the given name, with its mark still on it. Neither form was particle vocabulary, so both read as a middle name. P6 asks only the particle tag, so the fix is vocabulary alone: - d', l', dell' and their U+2019 spellings, plus de’ beside de' - al-, ul-, the article assimilated to each sun letter (ASCII and DMG spellings) and the Egyptian el- forms, as a closed list All never-given: written apart, the word is the article and nothing else. The family renders with a space as every particle joins ("d' Ivoi"); glued forms ("d'Ivoi") are unchanged. 39 corpus names move, every one the same shape (38 GND, 1 conventions); the GND and conventions agreement records gain the progress and are re-recorded. Co-Authored-By: Claude Opus 5.5 --- docs/design/decisions.md | 4 + docs/design/rules.md | 11 + docs/release_log.rst | 1 + nameparser/config/particles.py | 23 + tests/v2/cases.py | 53 ++ tests/v2/test_facade_cases.py | 3 + tools/differential/conventions_agreement.json | 15 - tools/differential/corpus_rules.jsonl | 4 + tools/differential/gnd_agreement.json | 570 ------------------ tools/differential/snapshot.jsonl | 80 +-- 10 files changed, 140 insertions(+), 624 deletions(-) diff --git a/docs/design/decisions.md b/docs/design/decisions.md index ebb61150..4d3d5edb 100644 --- a/docs/design/decisions.md +++ b/docs/design/decisions.md @@ -172,6 +172,10 @@ Decided 2026-08-19 (was Open: [#380](https://github.com/derek73/python-nameparse NOT DECIDED HERE: the mixed-case `Doe, Jane PhD vd Ma` (middle 'vd Ma', silent), the issue's fourth bullet. Its reading is unchanged; whether the family-comma path should report the chain's fork there is a separate question. Open: #578 (decided the same day, the S2 section's #578 entry: silent, as it reads). Nor the positional read of a part before a comma followed by no name word, where the case still splits the word: `Smith vd Ma, Prof.` reads given 'Smith', family 'vd Ma', and `SMITH VD MA, PROF.` family 'SMITH', suffix 'VD MA', on this tree and on `e56dd296` alike — that part reads as the comma-less name does, and the comma-less name splits the same way. BLAST RADIUS, measured 2026-10-02 with `e56dd296` (the pre-change master) as the comparator. The gate's output is byte-identical at all five baselines apart from the tree path: no corpus name holds these shapes, the rules.md examples aside. Over a generated grid, 5,676 of 409,498 parses move, every one in one of two classes — 3,534 where a family gains the words behind its particle, 2,142 where `vd` or `mc` leaves the family for the credential run — and no list of ambiguity kinds changes; 1,310 of the movers are written in mixed case. Recompute: from the pool `vd mc de van do Ma MA PhD Jr John Berg V la Prof.`, build every `<1 or 2 words>, `, `<3 words>, <1 word>`, `Smith , John`, `Doe, Jane <2-4 words>` and `Smith, <2-4 words>`; deduplicate, then parse each as written, `.upper()` and `.lower()`, keyed by the resulting text; keep the entries whose `as_dict()` or sorted ambiguity kinds differ between the trees, and class each by whether the new family holds every old family word and more, or the old family lost only `vd`/`mc` while the suffix kept every word. Cost: the given-slot test was QUADRATIC as first written, rescanning the particle run once per member — the second review measured 29,670 → 165,591 frames on `SMITH, VD ` + `DE `×k + `MA `×k at k = 256 against `e56dd296`, where an earlier wall-clock check at k = 100 and 400 had read 3.18× and passed for linear. The run's end is now kept once per title set: 1.90–1.99× per doubling of k from 16 to 256, and `_REREAD_SHAPES["lead"]` in tests/v2/test_benchmark.py guards it (generator frames at 8 units against 32: 3.38× kept, 7.53× rescanning, bound 6.0). The frame-count reference band did not move. - 2026-10-06 #613 — THE COMMA SITE IS DECIDED IN THE PASS THAT READS THE GIVEN PART. #611 found P6 showing the decisive sign smaller: assign held back from #602's run exactly the particle tail post_rules would attach a stage later, by asking `particle_tail` over roles that meant "not yet read" there and "suffix" in post_rules. The attachment and its third exception (`_inside_a_credential_run`) moved into assign, right after the given-part walk, and `particle_tail` is asked ONCE, at the moment the walk reaches #602's run: the pieces in front of the run hold the walk's roles and the run's words none, the state in which its answer equals the one over the finished roles (argued piece by piece in its docstring). post_rules ran P1's leading fold before P6, so assign asks P1's condition itself (`is_lone_never_given_particle` on the given piece), which is what keeps `Smith, de Mesnil van` the fold's. H1 and M4, the other rules ahead of the old site, are gated on there being no family, which a family comma always has. The no-comma site (#467) stays in post_rules, since it runs over the order P1's family-first redistribution leaves. NOTHING MOVES, measured 2026-10-06 against the commit before it: the P6 grid (103,786 distinct texts: each of the heads `Berg, Jan`, `Berg, Jan Piet`, `BERG, JAN`, `Berg, Dr. Jan`, `Berg, de`, `Berg, van`, `Berg, abdul`, `Smith, de Mesnil`, `de la, Jan` and `Nguyen, Thi`, followed by every sequence of zero to three of `van de der vd VD mc do DO Do PhD Jr. MA Ma Prof. V la Piet`, with and without a trailing `, Jr.` -- under the default policy, `lenient_comma_suffixes=False`, a family-first order and `middle_as_family`; comparator every token's role and folded or unjoined tag, every ambiguity's kind, detail and token IN ORDER, and `initials()`) and the #613 harness's 80,423 texts are identical, the gate's output byte-identical at all five baselines but for the baseline path, and the suite unchanged. ONE SHAPE DID MOVE, and the PR review found it on a grid with an EMPTY part before the comma, a head no measurement above held: there no family is named, H1 and M4 are not gated off, and they had made the given word the family before the old site attached -- `, Mr. Jones vd` read title 'Mr.', family 'Jones', suffix 'vd', and assign attaching first read given 'Jones', family 'vd' (177 texts on that grid). Reading P6 strictly instead (no family named, no attachment) moved 1,302 texts against master, `, Jan van` among them, so the attachment behind an empty family part stays in post_rules, after H1 and M4, through the same helper; that grid now differs from b547c247 only in C1's decided classes. Frames drop on every family-comma name, post_rules' block having paid a second `particle_tail` and a generator-driven search for the given word on every one, tail or not, where assign now attaches only behind a non-empty tail: `Smith, John` 169 → 160, `Smith, John, Jr.` 213 → 204, `Doe, Jane nee Smith` 302 → 293, a no-comma name unchanged. +- 2026-10-17 #700 — THE ELIDED PARTICLE AND THE HYPHEN-ATTACHED ARABIC ARTICLE ARE PARTICLE VOCABULARY, NEVER-GIVEN, AND THE RULE IS UNCHANGED. Library catalogs file `Paul d'Ivoi` as `Ivoi, Paul d'` and `Ḥusnī az-Zaʿīm` as `Zaʿīm, Ḥusnī az-`, the particle split off with its mark still on it; both read as a middle name (1.4.0 and 2.4 alike) because neither written form was in `PARTICLES`. P6 asks only the particle tag, so the fix is the vocabulary alone: no code moved. Three decisions, each taken by Derek on the #700 session's options. ELIDED SET: `d'`, `l'`, `dell'`, each also in its U+2019 spelling, and `de’` beside the existing `de'`, since nothing folds the typographic apostrophe to the ASCII one. ARTICLES: a CLOSED list — `al-`, `ul-`, the article assimilated to each of the fourteen sun letters in ASCII and DMG spellings (`ar-`, `az-`, `aš-`, `ash-`, `aṭ-`, …), and the Egyptian `el-` with its assimilations (`es-`, `ed-`, …) — all never-given, where the bare `al` stays ambiguous (Al Gore): written apart with its hyphen, the word is the article and nothing else. RENDERING: the attached family joins with a space like every particle, `d' Ivoi` and `al- Rashid`. + Declined: a shape rule (a vowel, consonants, a hyphen) in place of the list. It would need a new predicate and the bare-word collision check the issue named, for a population the list already covers; the corpora attest seven article spellings (`al- az- aš- ar- es- as- aṭ-`), all in it. + Not decided here: the GLUED forms (`Paul d'Ivoi`, `Harun al-Rashid`) stay one word with no particle split, out of #700's scope by its own statement. Hyphen-minus only: U+2010 and the non-breaking hyphen are not listed. The Scottish Gaelic `a’` (`Mac a’ Ghobhainn`, in corpus_cldr.jsonl) is mid-name and not an index-form particle, and was not added. + Measured 2026-10-17 against the parent commit `b4a004c3`: 39 corpus names move, every one the same shape — a trailing `d'` or article leaves the middle name and joins the family (38 in corpus_gnd.jsonl, `Orval, Pierre d'` in corpus_conventions.jsonl), and no ambiguity kind changes. Recompute: `uv run python tools/differential/snapshot.py --since b4a004c3`. That is evidence about the corpus as much as the change: these are the comma-bearing names ending in one of the new forms, and GND's index forms are where they occur. ### P7 — a one-letter particle against the initial it spells diff --git a/docs/design/rules.md b/docs/design/rules.md index f5538407..a3632ac9 100644 --- a/docs/design/rules.md +++ b/docs/design/rules.md @@ -949,6 +949,17 @@ P6. Rationale: a particle ending the name has nothing to link "doe, jane phd vd ma" → suffix="phd vd ma" "DOE, JANE PHD VD" → family="VD DOE" · boundary "DOE, JANE MA VD PHD" → suffix="MA VD PHD" + A particle an index form writes apart with its elision or its + hyphen still on it — the elided "d'" and the Arabic article in + its assimilated spellings, "al-" — is a particle like any other + here, and never a given name: written apart, the word is the + article and nothing else. It joins with a space like every + particle; written glued to the family ("d'Ivoi") it is one word + and this rule never sees it. + "Ivoi, Paul d'" → family="d' Ivoi" + "Rashid, Harun al-" → family="al- Rashid" + "Zaʿīm, Ḥusnī az-" → family="az- Zaʿīm" + "Ivoi, d'" → family="Ivoi d'" · boundary Without a comma, a declared family-first order has named the family in the same way and the attachment fires there too — but only where the run ENDS the name and stands in a MIDDLE — the one diff --git a/docs/release_log.rst b/docs/release_log.rst index 55809d12..73cbc5fd 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -11,6 +11,7 @@ Release Log - **Add Chinese and Japanese credential, office and generational words as suffixes.** ``彭德威 议员`` under ``locales.ZH`` gives family ``彭``, given ``德威``, suffix ``议员``, where 2.1 through 2.4.2 gave middle ``议员``; ``王雅婷,議員`` gives suffix ``議員``, where it gave given ``議員``. Read when written as a separate word or after a comma; no peel splits them off a glued name (``陳怡君二世`` is unchanged), though the Chinese pack's surname split exposes them after a listed surname (``王二世``). :issue:`671` - **Add the Akan and Indonesian salutations owura and bapak as titles.** ``Bapak Dwi Putro`` gives title ``Bapak``, first ``Dwi``, last ``Putro``, where 1.4.0 through 2.4.2 gave first ``Bapak``, middle ``Dwi``. :issue:`662` - **Fix a title written before a family comma reading as part of the family name.** ``Dr. Smith, John`` gives title ``Dr.``, last ``Smith``, where 1.4.0 through 2.4.2 gave last ``Dr. Smith``. Only listed titles are read there, so ``Xyz. Smith, John`` keeps its family, and a title that is also a surname particle, such as ``St`` or ``Freiherr``, stays in the surname. :issue:`701` + - **Fix a surname particle such as d' or al- reading as a middle name when a listing puts it after the given name.** ``Ivoi, Paul d'`` gives last ``d' Ivoi``, where 1.4.0 through 2.4.2 gave middle ``d'``; ``Zaʿīm, Ḥusnī az-`` gives last ``az- Zaʿīm``, and the Arabic article reads the same in its other assimilated spellings. A particle written joined to the surname, as in ``Paul d'Ivoi``, is unchanged. :issue:`700` - **Add Gov as a title.** ``Gov Smith`` gives title ``Gov``, last ``Smith``, where 1.4.0 through 2.4.2 gave first ``Gov``, and ``Lt. Gov. Smith, John`` gives title ``Lt. Gov.`` with the change above. :issue:`701` * 2.4.2 - October 13, 2026 diff --git a/nameparser/config/particles.py b/nameparser/config/particles.py index 672117fe..efc378ef 100644 --- a/nameparser/config/particles.py +++ b/nameparser/config/particles.py @@ -69,6 +69,29 @@ # Odia surname -- trailing, so it never reaches this rule. # The case that forced C-i's positional qualifier. "de'", # Italian elided "dei" ("de' Medici") + 'de’', # the same, typographic apostrophe: nothing folds U+2019 + # to U+0027, so each elided entry is listed in both (#700) + "d'", # French/Italian elided "de" ("Ivoi, Paul d'"). Written + 'd’', # apart only where an index form moves it behind the + # given name; glued ("d'Ivoi") it stays one word (#700) + "dell'", # Italian elided "dello"/"della" ("dell'Acqua") + 'dell’', + "l'", # French/Italian elided article ("L'Enfant") + 'l’', + # #700: the Arabic article as an index form writes it, hyphen + # attached and set behind the given name ("Rashid, Harun al-", + # "Zaʿīm, Ḥusnī az-"), in its assimilated spellings: 'al-' before a + # moon letter, assimilated to each of the fourteen sun letters + # (DMG's ṯ ḏ š ṣ ḍ ṭ ẓ beside the ASCII digraphs), and the Egyptian + # 'el-' and its assimilations. The trailing hyphen is the license: + # written apart, the word is the article and nothing else, so these + # are never-given where the bare 'al' (Al Gore) is AMBIGUOUS. A + # closed list rather than a shape rule, by decision on #700. ASCII + # hyphen-minus only. + 'al-', 'ul-', + 'at-', 'aṯ-', 'ath-', 'ad-', 'aḏ-', 'adh-', 'ar-', 'az-', 'as-', + 'aš-', 'ash-', 'aṣ-', 'aḍ-', 'aṭ-', 'aẓ-', 'an-', + 'el-', 'et-', 'ed-', 'er-', 'ez-', 'es-', 'en-', 'degli', # Italian "of the", masc. pl. 'dei', # Italian "of the", masc. pl. 'delle', # Italian "of the", fem. pl. diff --git a/tests/v2/cases.py b/tests/v2/cases.py index 95043469..536cd7bc 100644 --- a/tests/v2/cases.py +++ b/tests/v2/cases.py @@ -1938,6 +1938,59 @@ def _check_cjk_shape_purity(self) -> None: "one report covers the whole run, named for 'van', the " "word that is ambiguous vocabulary ('der' is never a " "given name)"), + # #700: the two written forms P6 had no vocabulary for -- the + # elided particle and the hyphen-attached Arabic article -- as + # library catalogs file them, behind the given name. + Case("an_elided_particle_after_a_family_comma", "Ivoi, Paul d'", + {"given": "Paul", "family": "d' Ivoi"}, + classification="fix(#700)", + notes="1.4.0 and 2.4 gave middle \"d'\". A never-given " + "particle, so the attachment reports nothing, as " + "'Jong, Anke de' does. The family renders with a space, " + "as every particle joins; the glued 'd'Ivoi' is one word " + "and out of #700's scope"), + Case("an_elided_particle_in_its_typographic_spelling", + "Ivoi, Paul d’", + {"given": "Paul", "family": "d’ Ivoi"}, + classification="fix(#700)", + notes="U+2019 is not folded to the ASCII apostrophe anywhere, " + "so each elided entry is listed in both spellings. " + "1.4.0 middle 'd’'"), + Case("an_elided_particle_beside_a_suffix", "Ivoi, Paul d', Jr.", + {"given": "Paul", "family": "d' Ivoi", "suffix": "Jr."}, + classification="fix(#700)", + notes="AGENTS.md's comma sweep: a suffix in its own part leaves " + "the given part's attachment alone. 1.4.0 middle \"d'\""), + Case("an_elided_particle_alone_after_a_family_comma", "Ivoi, d'", + {"family": "Ivoi d'"}, + notes="boundary: no given word remains to spare, so P6 does not " + "attach -- the part reads as 'Jong, de' does, family " + "'Jong de'"), + Case("the_arabic_article_after_a_family_comma", "Rashid, Harun al-", + {"given": "Harun", "family": "al- Rashid"}, + classification="fix(#700)", + notes="1.4.0 and 2.4 gave middle 'al-'. The hyphen is what makes " + "'al-' never-given where the bare 'al' (Al Gore) is " + "ambiguous, so no fork is reported"), + Case("an_assimilated_arabic_article_after_a_family_comma", + "Zaʿīm, Ḥusnī az-", + {"given": "Ḥusnī", "family": "az- Zaʿīm"}, + classification="fix(#700)", + notes="the article before a sun letter, in DMG spelling as GND " + "writes it. 1.4.0 middle 'az-'"), + Case("an_elided_particle_ending_a_family_first_name", "Ivoi Paul d'", + {"given": "Paul", "family": "d' Ivoi"}, + policy=Policy(name_order=FAMILY_FIRST), + classification="fix(#700)", + notes="P6's family-first site reads the same vocabulary as its " + "comma site, so the comma-less writing parses alike"), + Case("an_elided_particle_written_apart_before_the_family", + "Paul d' Ivoi", + {"given": "Paul", "family": "d' Ivoi"}, + classification="fix(#700)", + notes="not #700's index form but the same vocabulary: the " + "particle chains forward as 'de' does. 1.4.0 middle " + "\"d'\""), Case("tussenvoegsel_outranks_the_suffix_reading", "Berg, Jan vd", {"given": "Jan", "family": "vd Berg"}, classification="fix(#380)", diff --git a/tests/v2/test_facade_cases.py b/tests/v2/test_facade_cases.py index f4294e7c..0629cb61 100644 --- a/tests/v2/test_facade_cases.py +++ b/tests/v2/test_facade_cases.py @@ -82,6 +82,9 @@ "family_comma_no_name_word_family_first", # #687: a declared Vietnamese order, which v1 cannot express "trailing_given_name_mai_is_not_a_credential", + # #700: P6's family-first site for the elided particle, under a + # declared order v1 has no spelling for + "an_elided_particle_ending_a_family_first_name", "family_comma_no_name_word_family_first_given_last", "family_comma_no_name_word_family_first_plain", "leading_never_given_particle_two_leftovers_family_first", diff --git a/tools/differential/conventions_agreement.json b/tools/differential/conventions_agreement.json index 91a03600..70227219 100644 --- a/tools/differential/conventions_agreement.json +++ b/tools/differential/conventions_agreement.json @@ -158,21 +158,6 @@ }, "text": "Murat Bey" }, - "conventions/Orval, Pierre d'/default": { - "family": { - "expected": "Orval d'", - "got": "Orval" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Pierre", - "got": "Pierre d'" - }, - "text": "Orval, Pierre d'" - }, "conventions/Park Ji-hoon ssi/FAMILY_FIRST": { "given_names": { "expected": "Ji-hoon", diff --git a/tools/differential/corpus_rules.jsonl b/tools/differential/corpus_rules.jsonl index b05e9c36..868b6bdd 100644 --- a/tools/differential/corpus_rules.jsonl +++ b/tools/differential/corpus_rules.jsonl @@ -123,6 +123,8 @@ "Her Royal Highness Princess Anne" "His Excellency Lord Duncan" "II Van Johnson" +"Ivoi, Paul d'" +"Ivoi, d'" "J-E-P DUPONT" "J. Smith" "J. née Jones Smith V" @@ -359,6 +361,7 @@ "Prof. Smith, John, PhD" "Pérez, Juan Ó." "QC MP" +"Rashid, Harun al-" "Rev. John Smith" "Rev. Sr. Smith, Mary" "Rinpoche" @@ -453,6 +456,7 @@ "Xyz. John Smith" "Xyz. Smith, John" "Xyz. van Johnson" +"Zaʿīm, Ḥusnī az-" "abd Allah" "abd Allah Smith" "abd Allah Smith née Jones" diff --git a/tools/differential/gnd_agreement.json b/tools/differential/gnd_agreement.json index 298186de..1b930853 100644 --- a/tools/differential/gnd_agreement.json +++ b/tools/differential/gnd_agreement.json @@ -1,19 +1,4 @@ { - "101063857/particle-elided/sorting": { - "family": { - "expected": "Arenberg d'", - "got": "Arenberg" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Auguste Marie Raymond", - "got": "Auguste Marie Raymond d'" - }, - "text": "Arenberg, Auguste Marie Raymond d'" - }, "1012101827/direct-is/givenFirst": { "family": { "expected": "", @@ -58,21 +43,6 @@ }, "text": "Konráð Gíslason" }, - "1017076367/particle-elided/sorting": { - "family": { - "expected": "Orléans-Longueville d'", - "got": "Orléans-Longueville" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Jean Louis Charles", - "got": "Jean Louis Charles d'" - }, - "text": "Orléans-Longueville, Jean Louis Charles d'" - }, "1017511667/regnal/givenFirst": { "family": { "expected": "", @@ -128,36 +98,6 @@ }, "text": "Christian Ulrich II." }, - "1019736747/particle-elided/sorting": { - "family": { - "expected": "Estrées d'", - "got": "Estrées" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Victor-Marie", - "got": "Victor-Marie d'" - }, - "text": "Estrées, Victor-Marie d'" - }, - "1020770929/particle-arab/sorting": { - "family": { - "expected": "Ǧaʿfarī al-", - "got": "Ǧaʿfarī" - }, - "family_particles": { - "expected": "al-", - "got": "" - }, - "given_names": { - "expected": "Ibrāhīm", - "got": "Ibrāhīm al-" - }, - "text": "Ǧaʿfarī, Ibrāhīm al-" - }, "1021332496/direct-ir-af/givenFirst": { "family": { "expected": "", @@ -180,36 +120,6 @@ }, "text": "Karl I." }, - "102371512/particle-arab/sorting": { - "family": { - "expected": "Maqrīzī al-", - "got": "Maqrīzī" - }, - "family_particles": { - "expected": "al-", - "got": "" - }, - "given_names": { - "expected": "Aḥmad Ibn-ʿAlī", - "got": "Aḥmad Ibn-ʿAlī al-" - }, - "text": "Maqrīzī, Aḥmad Ibn-ʿAlī al-" - }, - "1025906977/particle-elided/sorting": { - "family": { - "expected": "Alzon d'", - "got": "Alzon" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Emmanuel", - "got": "Emmanuel d'" - }, - "text": "Alzon, Emmanuel d'" - }, "1026632668/direct-et-mm/givenFirst": { "family": { "expected": "", @@ -529,21 +439,6 @@ }, "text": "Simkoyê Şikak" }, - "1081737697/particle-elided/sorting": { - "family": { - "expected": "Annebault d'", - "got": "Annebault" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Claude", - "got": "Claude d'" - }, - "text": "Annebault, Claude d'" - }, "1083655477/compound-latam/givenFirst": { "family": { "expected": "Málaga de Prado", @@ -577,36 +472,6 @@ }, "text": "Malik Ambar" }, - "1112629513/particle-arab/sorting": { - "family": { - "expected": "Zaʿīm az-", - "got": "Zaʿīm" - }, - "family_particles": { - "expected": "az-", - "got": "" - }, - "given_names": { - "expected": "Ḥusnī", - "got": "Ḥusnī az-" - }, - "text": "Zaʿīm, Ḥusnī az-" - }, - "111277957/particle-fr/sorting": { - "family": { - "expected": "Ivoi d'", - "got": "Ivoi" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Paul", - "got": "Paul d'" - }, - "text": "Ivoi, Paul d'" - }, "1115673882/direct-arab/givenFirst": { "family": { "expected": "", @@ -673,21 +538,6 @@ }, "text": "Magnús Eiríksson" }, - "1139846442/particle-arab/sorting": { - "family": { - "expected": "Rikābī ar-", - "got": "Rikābī" - }, - "family_particles": { - "expected": "ar-", - "got": "" - }, - "given_names": { - "expected": "Fuʾād", - "got": "Fuʾād ar-" - }, - "text": "Rikābī, Fuʾād ar-" - }, "1143145518/direct-arab/givenFirst": { "family": { "expected": "", @@ -721,21 +571,6 @@ }, "text": "Tadsch ad-Din al-Hasani" }, - "1147042357/particle-elided/sorting": { - "family": { - "expected": "Hooghvorst d'", - "got": "Hooghvorst" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Emmanuel", - "got": "Emmanuel d'" - }, - "text": "Hooghvorst, Emmanuel d'" - }, "1147649030/direct-my-id/givenFirst": { "family": { "expected": "", @@ -747,21 +582,6 @@ }, "text": "Mohamed Khir Johari" }, - "1155661249/particle-arab/sorting": { - "family": { - "expected": "Saad es-", - "got": "Saad" - }, - "family_particles": { - "expected": "es-", - "got": "" - }, - "given_names": { - "expected": "Habib Pacha", - "got": "Habib Pacha es-" - }, - "text": "Saad, Habib Pacha es-" - }, "1155767624/direct-ir-af/givenFirst": { "family": { "expected": "", @@ -894,36 +714,6 @@ }, "text": "Hussein Kamil" }, - "117606227/particle-it/sorting": { - "family": { - "expected": "Orsi d'", - "got": "Orsi" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Achille", - "got": "Achille d'" - }, - "text": "Orsi, Achille d'" - }, - "117763497/particle-it/sorting": { - "family": { - "expected": "Arco d'", - "got": "Arco" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Giovanni Battista Gherardo", - "got": "Giovanni Battista Gherardo d'" - }, - "text": "Arco, Giovanni Battista Gherardo d'" - }, "118049062/direct-is/givenFirst": { "family": { "expected": "", @@ -946,21 +736,6 @@ }, "text": "Abu-'l-Fidāʾ Ismāʿīl Ibn-ʿAlī" }, - "118501437/particle-elided/sorting": { - "family": { - "expected": "Albert d'", - "got": "Albert" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Eugen", - "got": "Eugen d'" - }, - "text": "Albert, Eugen d'" - }, "118513257/regnal/givenFirst": { "family": { "expected": "", @@ -983,21 +758,6 @@ }, "text": "Eduard V." }, - "118531107/particle-elided/sorting": { - "family": { - "expected": "Escoto d'", - "got": "Escoto" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Miguel", - "got": "Miguel d'" - }, - "text": "Escoto, Miguel d'" - }, "118534947/regnal/givenFirst": { "family": { "expected": "", @@ -1031,21 +791,6 @@ }, "text": "José Luis Martín Descalzo" }, - "1185824316/particle-arab/sorting": { - "family": { - "expected": "Faḥḥām al-", - "got": "Faḥḥām" - }, - "family_particles": { - "expected": "al-", - "got": "" - }, - "given_names": { - "expected": "Šākir", - "got": "Šākir al-" - }, - "text": "Faḥḥām, Šākir al-" - }, "118587587/direct-eu/givenFirst": { "family": { "expected": "", @@ -1057,21 +802,6 @@ }, "text": "Ngugi wa Thiong'o" }, - "118604740/particle-arab/sorting": { - "family": { - "expected": "Sādāt as-", - "got": "Sādāt" - }, - "family_particles": { - "expected": "as-", - "got": "" - }, - "given_names": { - "expected": "Ǧīhān", - "got": "Ǧīhān as-" - }, - "text": "Sādāt, Ǧīhān as-" - }, "118624571/direct-et-mm/givenFirst": { "family": { "expected": "", @@ -1083,21 +813,6 @@ }, "text": "Gudina Tumsa" }, - "118650947/particle-fr/sorting": { - "family": { - "expected": "Aubigné d'", - "got": "Aubigné" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Théodore Agrippa", - "got": "Théodore Agrippa d'" - }, - "text": "Aubigné, Théodore Agrippa d'" - }, "118668757/compound-iberia/givenFirst": { "family": { "expected": "Castelo Branco", @@ -1186,21 +901,6 @@ }, "text": "Ṯābit Ibn-Qurra" }, - "118848062/particle-arab/sorting": { - "family": { - "expected": "Assad al-", - "got": "Assad" - }, - "family_particles": { - "expected": "al-", - "got": "" - }, - "given_names": { - "expected": "Hafis", - "got": "Hafis al-" - }, - "text": "Assad, Hafis al-" - }, "118850555/direct-ir-af/givenFirst": { "family": { "expected": "", @@ -1212,21 +912,6 @@ }, "text": "Ṣafī-ad-Dīn Ardabīlī" }, - "118857770/particle-arab/sorting": { - "family": { - "expected": "Buḥturī al-", - "got": "Buḥturī" - }, - "family_particles": { - "expected": "al-", - "got": "" - }, - "given_names": { - "expected": "al-Walīd Ibn-ʿUbaid", - "got": "al-Walīd Ibn-ʿUbaid al-" - }, - "text": "Buḥturī, al-Walīd Ibn-ʿUbaid al-" - }, "1188659936/direct-ir-af/givenFirst": { "family": { "expected": "", @@ -1315,21 +1000,6 @@ }, "text": "Manuel Marulanda Vélez" }, - "119007703/particle-arab/sorting": { - "family": { - "expected": "Ġazālī al-", - "got": "Ġazālī" - }, - "family_particles": { - "expected": "al-", - "got": "" - }, - "given_names": { - "expected": "Zainab", - "got": "Zainab al-" - }, - "text": "Ġazālī, Zainab al-" - }, "119022397/regnal/givenFirst": { "family": { "expected": "", @@ -1452,36 +1122,6 @@ }, "text": "Zarea Yaeqob" }, - "119177447/particle-elided/sorting": { - "family": { - "expected": "Orléans d'", - "got": "Orléans" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Ferdinand-Philippe", - "got": "Ferdinand-Philippe d'" - }, - "text": "Orléans, Ferdinand-Philippe d'" - }, - "119192047/particle-iberia/sorting": { - "family": { - "expected": "Maǧrīṭī al-", - "got": "Maǧrīṭī" - }, - "family_particles": { - "expected": "al-", - "got": "" - }, - "given_names": { - "expected": "Abu-'l-Qāsim Maslama Ibn-Aḥmad", - "got": "Abu-'l-Qāsim Maslama Ibn-Aḥmad al-" - }, - "text": "Maǧrīṭī, Abu-'l-Qāsim Maslama Ibn-Aḥmad al-" - }, "119196824/direct-ir-af/givenFirst": { "family": { "expected": "", @@ -1548,21 +1188,6 @@ }, "text": "Eitel Friedrich II." }, - "119272911/particle-arab/sorting": { - "family": { - "expected": "Ḥakīm al-", - "got": "Ḥakīm" - }, - "family_particles": { - "expected": "al-", - "got": "" - }, - "given_names": { - "expected": "Taufīq", - "got": "Taufīq al-" - }, - "text": "Ḥakīm, Taufīq al-" - }, "119288109/direct-et-mm/givenFirst": { "family": { "expected": "", @@ -1607,36 +1232,6 @@ }, "text": "Shemuʾel ben Ḥofni" }, - "119418029/particle-arab/sorting": { - "family": { - "expected": "Zaiyāt az-", - "got": "Zaiyāt" - }, - "family_particles": { - "expected": "az-", - "got": "" - }, - "given_names": { - "expected": "Laṭīfa", - "got": "Laṭīfa az-" - }, - "text": "Zaiyāt, Laṭīfa az-" - }, - "119426838/particle-arab/sorting": { - "family": { - "expected": "Ḫarrāṭ al-", - "got": "Ḫarrāṭ" - }, - "family_particles": { - "expected": "al-", - "got": "" - }, - "given_names": { - "expected": "Idwār", - "got": "Idwār al-" - }, - "text": "Ḫarrāṭ, Idwār al-" - }, "119436167/particle-it/sorting": { "family": { "expected": "Pèrs di", @@ -1711,36 +1306,6 @@ }, "text": "Werner I." }, - "1199143707/particle-elided/sorting": { - "family": { - "expected": "Orville d'", - "got": "Orville" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Johann Friedrich", - "got": "Johann Friedrich d'" - }, - "text": "Orville, Johann Friedrich d'" - }, - "120010887/particle-elided/sorting": { - "family": { - "expected": "Angoulême d'", - "got": "Angoulême" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Louis Antoine d'Artois", - "got": "Louis Antoine d'Artois d'" - }, - "text": "Angoulême, Louis Antoine d'Artois d'" - }, "1200607953/direct-et-mm/givenFirst": { "family": { "expected": "", @@ -1921,21 +1486,6 @@ }, "text": "Sarkīs ar-Rizzī" }, - "123439647/particle-elided/sorting": { - "family": { - "expected": "Alquen d'", - "got": "Alquen" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Gunter", - "got": "Gunter d'" - }, - "text": "Alquen, Gunter d'" - }, "123505585/direct-my-id/givenFirst": { "family": { "expected": "", @@ -1980,36 +1530,6 @@ }, "text": "Diego Carrillo de Mendoza y Pimentel" }, - "123907373/particle-arab/sorting": { - "family": { - "expected": "Šiblī aš-", - "got": "Šiblī" - }, - "family_particles": { - "expected": "aš-", - "got": "" - }, - "given_names": { - "expected": "Abū-Bakr", - "got": "Abū-Bakr aš-" - }, - "text": "Šiblī, Abū-Bakr aš-" - }, - "124211917/particle-fr/sorting": { - "family": { - "expected": "Auxiron d'", - "got": "Auxiron" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Claude François Joseph", - "got": "Claude François Joseph d'" - }, - "text": "Auxiron, Claude François Joseph d'" - }, "124714617/direct-is/givenFirst": { "family": { "expected": "", @@ -2021,21 +1541,6 @@ }, "text": "Svavar Guðnason" }, - "124788165/particle-arab/sorting": { - "family": { - "expected": "Yāzīǧī al-", - "got": "Yāzīǧī" - }, - "family_particles": { - "expected": "al-", - "got": "" - }, - "given_names": { - "expected": "Ibrāhīm", - "got": "Ibrāhīm al-" - }, - "text": "Yāzīǧī, Ibrāhīm al-" - }, "124955487/compound-latam/givenFirst": { "family": { "expected": "Prats Gonzaléz", @@ -2150,21 +1655,6 @@ }, "text": "Shahnon Ahmad" }, - "128897732/particle-arab/sorting": { - "family": { - "expected": "Ḥāǧǧ al-", - "got": "Ḥāǧǧ" - }, - "family_particles": { - "expected": "al-", - "got": "" - }, - "given_names": { - "expected": "Kamāl Yūsuf", - "got": "Kamāl Yūsuf al-" - }, - "text": "Ḥāǧǧ, Kamāl Yūsuf al-" - }, "129229970/direct-is/givenFirst": { "family": { "expected": "", @@ -2209,21 +1699,6 @@ }, "text": "Anan ben David" }, - "129765767/particle-nl-be/sorting": { - "family": { - "expected": "Outrein d'", - "got": "Outrein" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Johannes", - "got": "Johannes d'" - }, - "text": "Outrein, Johannes d'" - }, "1298312620/direct-my-id/givenFirst": { "family": { "expected": "", @@ -2246,21 +1721,6 @@ }, "text": "Charles Claude de La Billarderie d'Angiviller" }, - "129854697/particle-elided/sorting": { - "family": { - "expected": "Angiviller d'", - "got": "Angiviller" - }, - "family_particles": { - "expected": "d'", - "got": "" - }, - "given_names": { - "expected": "Charles Claude de La Billarderie", - "got": "Charles Claude de La Billarderie d'" - }, - "text": "Angiviller, Charles Claude de La Billarderie d'" - }, "130656097/direct-my-id/givenFirst": { "family": { "expected": "", @@ -2272,21 +1732,6 @@ }, "text": "Abdullah bin Haji Ahmad Badawi" }, - "130897035/particle-arab/sorting": { - "family": { - "expected": "Ṭihrānī aṭ-", - "got": "Ṭihrānī" - }, - "family_particles": { - "expected": "aṭ-", - "got": "" - }, - "given_names": { - "expected": "Āqā Buzurg", - "got": "Āqā Buzurg aṭ-" - }, - "text": "Ṭihrānī, Āqā Buzurg aṭ-" - }, "131443097/compound-iberia/givenFirst": { "family": { "expected": "Robles Pazos", @@ -2566,21 +2011,6 @@ }, "text": "ʿAbdallāh Ibn-ʿAbdalqādir" }, - "151529558/particle-arab/sorting": { - "family": { - "expected": "Šaʿrāwī aš-", - "got": "Šaʿrāwī" - }, - "family_particles": { - "expected": "aš-", - "got": "" - }, - "given_names": { - "expected": "Hudā", - "got": "Hudā aš-" - }, - "text": "Šaʿrāwī, Hudā aš-" - }, "170741427/compound-iberia/givenFirst": { "family": { "expected": "Garaikoetxea Urriza", diff --git a/tools/differential/snapshot.jsonl b/tools/differential/snapshot.jsonl index 9328c606..001e2973 100644 --- a/tools/differential/snapshot.jsonl +++ b/tools/differential/snapshot.jsonl @@ -101,7 +101,7 @@ {"name": "Albert Bonet Marrugat", "given": "Albert", "middle": "Bonet", "family": "Marrugat", "initials": "A. B. M."} {"name": "Albert Guðmundsson", "given": "Albert", "family": "Guðmundsson", "initials": "A. G."} {"name": "Albert Lederle", "given": "Albert", "family": "Lederle", "initials": "A. L."} -{"name": "Albert, Eugen d'", "given": "Eugen", "middle": "d'", "family": "Albert", "initials": "E. d. A."} +{"name": "Albert, Eugen d'", "given": "Eugen", "family": "d' Albert", "initials": "E. A."} {"name": "Albertino della Piazza", "given": "Albertino", "family": "della Piazza", "initials": "A. P."} {"name": "Alberto Abello Vives", "given": "Alberto", "middle": "Abello", "family": "Vives", "initials": "A. A. V."} {"name": "Alberto García Ulecia", "given": "Alberto", "middle": "García", "family": "Ulecia", "initials": "A. G. U."} @@ -134,9 +134,9 @@ {"name": "Alois von und zu Liechtenstein", "given": "Alois", "family": "von und zu Liechtenstein", "initials": "A. L."} {"name": "Alois von und zu und von Liechtenstein", "given": "Alois", "family": "von und zu und von Liechtenstein", "initials": "A. L."} {"name": "Alonso de Cardenas", "given": "Alonso", "family": "de Cardenas", "initials": "A. C."} -{"name": "Alquen, Gunter d'", "given": "Gunter", "middle": "d'", "family": "Alquen", "initials": "G. d. A."} +{"name": "Alquen, Gunter d'", "given": "Gunter", "family": "d' Alquen", "initials": "G. A."} {"name": "Alvensleben, Albrecht von", "given": "Albrecht", "family": "von Alvensleben", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "A. A."} -{"name": "Alzon, Emmanuel d'", "given": "Emmanuel", "middle": "d'", "family": "Alzon", "initials": "E. d. A."} +{"name": "Alzon, Emmanuel d'", "given": "Emmanuel", "family": "d' Alzon", "initials": "E. A."} {"name": "Amadi John Chinedu", "order": "FAMILY_FIRST", "given": "John", "middle": "Chinedu", "family": "Amadi", "initials": "J. C. A."} {"name": "Amadi Maazị Christopher Chukwudike Jr, MP", "order": "FAMILY_FIRST", "given": "Maazị", "middle": "Christopher Chukwudike", "family": "Amadi", "suffix": "Jr, MP", "initials": "M. C. C. A."} {"name": "Amadi, Christopher Chukwudike", "given": "Christopher", "middle": "Chukwudike", "family": "Amadi", "initials": "C. C. A."} @@ -179,8 +179,8 @@ {"name": "Andrews, M.D.", "family": "Andrews", "suffix": "M.D.", "initials": "A."} {"name": "Andrés David Padilla Mejía", "given": "Andrés", "middle": "David Padilla", "family": "Mejía", "initials": "A. D. P. M."} {"name": "Ane Miren Urrestaratzu", "given": "Ane", "middle": "Miren", "family": "Urrestaratzu", "initials": "A. M. U."} -{"name": "Angiviller, Charles Claude de La Billarderie d'", "given": "Charles", "middle": "Claude de La Billarderie d'", "family": "Angiviller", "initials": "C. C. B. d. A."} -{"name": "Angoulême, Louis Antoine d'Artois d'", "given": "Louis", "middle": "Antoine d'Artois d'", "family": "Angoulême", "initials": "L. A. d. d. A."} +{"name": "Angiviller, Charles Claude de La Billarderie d'", "given": "Charles", "middle": "Claude de La Billarderie", "family": "d' Angiviller", "initials": "C. C. B. A."} +{"name": "Angoulême, Louis Antoine d'Artois d'", "given": "Louis", "middle": "Antoine d'Artois", "family": "d' Angoulême", "initials": "L. A. d. A."} {"name": "Anh", "given": "Anh", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "A."} {"name": "Anh", "order": "FAMILY_FIRST", "family": "Anh", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "A."} {"name": "Anh Do", "given": "Anh", "family": "Do", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "A. D."} @@ -204,7 +204,7 @@ {"name": "Anne Sepp", "given": "Anne", "family": "Sepp", "initials": "A. S."} {"name": "Anne Sofie Holm Hansen", "given": "Anne", "middle": "Sofie Holm", "family": "Hansen", "initials": "A. S. H. H."} {"name": "Anne-Pierre de Montesquiou-Fézensac", "given": "Anne-Pierre", "family": "de Montesquiou-Fézensac", "initials": "A. M."} -{"name": "Annebault, Claude d'", "given": "Claude", "middle": "d'", "family": "Annebault", "initials": "C. d. A."} +{"name": "Annebault, Claude d'", "given": "Claude", "family": "d' Annebault", "initials": "C. A."} {"name": "Annelise Weber", "given": "Annelise", "family": "Weber", "initials": "A. W."} {"name": "Annette Charlotte Freiherrin von und zu der Tann-Rathsamhausen", "given": "Annette", "middle": "Charlotte", "family": "Freiherrin von und zu der Tann-Rathsamhausen", "initials": "A. C. T."} {"name": "Antico", "given": "Antico", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "A."} @@ -228,10 +228,10 @@ {"name": "Anuar bin Malek", "given": "Anuar", "family": "bin Malek", "initials": "A. M."} {"name": "Anwar Ibrahim", "given": "Anwar", "family": "Ibrahim", "initials": "A. I."} {"name": "Arcadio Poveda Ricalde", "given": "Arcadio", "middle": "Poveda", "family": "Ricalde", "initials": "A. P. R."} -{"name": "Arco, Giovanni Battista Gherardo d'", "given": "Giovanni", "middle": "Battista Gherardo d'", "family": "Arco", "initials": "G. B. G. d. A."} +{"name": "Arco, Giovanni Battista Gherardo d'", "given": "Giovanni", "middle": "Battista Gherardo", "family": "d' Arco", "initials": "G. B. G. A."} {"name": "Ardelean, Daniel Ionescu Dragoș Ioan", "given": "Daniel", "middle": "Ionescu Dragoș Ioan", "family": "Ardelean", "initials": "D. I. D. I. A."} {"name": "Arenas Betancourt, Rodrigo", "given": "Rodrigo", "family": "Arenas Betancourt", "initials": "R. A. B."} -{"name": "Arenberg, Auguste Marie Raymond d'", "given": "Auguste", "middle": "Marie Raymond d'", "family": "Arenberg", "initials": "A. M. R. d. A."} +{"name": "Arenberg, Auguste Marie Raymond d'", "given": "Auguste", "middle": "Marie Raymond", "family": "d' Arenberg", "initials": "A. M. R. A."} {"name": "Arend, Arie den", "given": "Arie", "family": "den Arend", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "A. A."} {"name": "Arie den Arend", "given": "Arie", "family": "den Arend", "initials": "A. A."} {"name": "Armando Domínguez Borrás", "given": "Armando", "middle": "Domínguez", "family": "Borrás", "initials": "A. D. B."} @@ -244,20 +244,20 @@ {"name": "Asas Manterola, Benita", "given": "Benita", "family": "Asas Manterola", "initials": "B. A. M."} {"name": "Asmar", "given": "Asmar", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "A."} {"name": "Asmar", "order": "FAMILY_FIRST", "family": "Asmar", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "A."} -{"name": "Assad, Hafis al-", "given": "Hafis", "middle": "al-", "family": "Assad", "initials": "H. a. A."} +{"name": "Assad, Hafis al-", "given": "Hafis", "family": "al- Assad", "initials": "H. A."} {"name": "Assoc Dean", "title": "Assoc", "family": "Dean", "initials": "D."} {"name": "Assoc Dean of Chemistry Robert Johns", "title": "Assoc", "given": "Dean of Chemistry", "middle": "Robert", "family": "Johns", "initials": "D. C. R. J."} {"name": "Asst. Vice Chancellor John Smith", "title": "Asst. Vice Chancellor", "given": "John", "family": "Smith", "initials": "J. S."} {"name": "Ataíde, Luís de", "given": "Luís", "family": "de Ataíde", "initials": "L. A."} {"name": "Attorney General of Minnesota", "title": "Attorney", "family": "General of Minnesota", "ambiguities": ["TITLE_OR_NAME"], "initials": "G. M."} -{"name": "Aubigné, Théodore Agrippa d'", "given": "Théodore", "middle": "Agrippa d'", "family": "Aubigné", "initials": "T. A. d. A."} +{"name": "Aubigné, Théodore Agrippa d'", "given": "Théodore", "middle": "Agrippa", "family": "d' Aubigné", "initials": "T. A. A."} {"name": "August von Cetto", "given": "August", "family": "von Cetto", "initials": "A. C."} {"name": "Auguste Marie Raymond d'Arenberg", "given": "Auguste", "middle": "Marie Raymond", "family": "d'Arenberg", "initials": "A. M. R. d."} {"name": "Augustus P. Gardner", "given": "Augustus", "middle": "P.", "family": "Gardner", "initials": "A. P. G."} {"name": "Augustus Seymour Porter", "given": "Augustus", "middle": "Seymour", "family": "Porter", "initials": "A. S. P."} {"name": "Aung San", "given": "Aung", "family": "San", "initials": "A. S."} {"name": "Aurelio García Lesmes", "given": "Aurelio", "middle": "García", "family": "Lesmes", "initials": "A. G. L."} -{"name": "Auxiron, Claude François Joseph d'", "given": "Claude", "middle": "François Joseph d'", "family": "Auxiron", "initials": "C. F. J. d. A."} +{"name": "Auxiron, Claude François Joseph d'", "given": "Claude", "middle": "François Joseph", "family": "d' Auxiron", "initials": "C. F. J. A."} {"name": "Avraham ben Mosheh ben Maimon", "given": "Avraham", "middle": "ben Mosheh ben", "family": "Maimon", "initials": "A. b. M. b. M."} {"name": "Awa Isseu", "order": "FAMILY_FIRST", "given": "Isseu", "family": "Awa", "initials": "I. A."} {"name": "Ayala Pérez, Daniel", "given": "Daniel", "family": "Ayala Pérez", "initials": "D. A. P."} @@ -399,7 +399,7 @@ {"name": "Bunn, Teddy", "given": "Teddy", "family": "Bunn", "initials": "T. B."} {"name": "Bunsen, Frances von", "given": "Frances", "family": "von Bunsen", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "F. B."} {"name": "Bustamante y Rivero, José Luis", "given": "José", "middle": "Luis", "family": "Bustamante y Rivero", "initials": "J. L. B. R."} -{"name": "Buḥturī, al-Walīd Ibn-ʿUbaid al-", "given": "al-Walīd", "middle": "Ibn-ʿUbaid al-", "family": "Buḥturī", "initials": "a. I. a. B."} +{"name": "Buḥturī, al-Walīd Ibn-ʿUbaid al-", "given": "al-Walīd", "middle": "Ibn-ʿUbaid", "family": "al- Buḥturī", "initials": "a. I. B."} {"name": "Bw. Adam Juma Agape Shukurani Masalu MB", "title": "Bw.", "given": "Adam", "middle": "Juma Agape Shukurani Masalu", "family": "MB", "initials": "A. J. A. S. M. M."} {"name": "Bābak Yaḥyā Ibn-Muʿāḏ al-Ḫurramī", "given": "Bābak", "middle": "Yaḥyā Ibn-Muʿāḏ", "family": "al-Ḫurramī", "initials": "B. Y. I. a."} {"name": "Bāqir Ḫān", "given": "Bāqir", "family": "Ḫān", "initials": "B. Ḫ."} @@ -828,13 +828,13 @@ {"name": "Eric H. Holder, Jr., Secretary of State", "title": "Secretary of State", "given": "Eric", "middle": "H.", "family": "Holder", "suffix": "Jr.", "ambiguities": ["COMMA_STRUCTURE"], "initials": "E. H. H."} {"name": "Eric de Bisschop", "given": "Eric", "family": "de Bisschop", "initials": "E. B."} {"name": "Ernst von Wildenbruch", "given": "Ernst", "family": "von Wildenbruch", "initials": "E. W."} -{"name": "Escoto, Miguel d'", "given": "Miguel", "middle": "d'", "family": "Escoto", "initials": "M. d. E."} +{"name": "Escoto, Miguel d'", "given": "Miguel", "family": "d' Escoto", "initials": "M. E."} {"name": "Esq Jane Smith", "given": "Esq", "middle": "Jane", "family": "Smith", "initials": "E. J. S."} {"name": "Esq Smith", "given": "Esq", "family": "Smith", "initials": "E. S."} {"name": "Esq. Smith", "title": "Esq.", "family": "Smith", "initials": "S."} {"name": "Esq. van Gogh", "title": "Esq.", "given": "van", "family": "Gogh", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "v. G."} {"name": "Esteva a Alsina, Maurus Daniel", "given": "Maurus", "middle": "Daniel", "family": "Esteva a Alsina", "initials": "M. D. E. a. A."} -{"name": "Estrées, Victor-Marie d'", "given": "Victor-Marie", "middle": "d'", "family": "Estrées", "initials": "V. d. E."} +{"name": "Estrées, Victor-Marie d'", "given": "Victor-Marie", "family": "d' Estrées", "initials": "V. E."} {"name": "Etiemble", "given": "Etiemble", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "E."} {"name": "Eugen d'Albert", "given": "Eugen", "family": "d'Albert", "initials": "E. d."} {"name": "Eugeni Aguiló i Pérez", "given": "Eugeni", "family": "Aguiló i Pérez", "initials": "E. A. P."} @@ -863,7 +863,7 @@ {"name": "Fatima Amina Umar", "given": "Fatima", "middle": "Amina", "family": "Umar", "initials": "F. A. U."} {"name": "Fatimah", "given": "Fatimah", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "F."} {"name": "Fauveau, Félicie de", "given": "Félicie", "family": "de Fauveau", "initials": "F. F."} -{"name": "Faḥḥām, Šākir al-", "given": "Šākir", "middle": "al-", "family": "Faḥḥām", "initials": "Š. a. F."} +{"name": "Faḥḥām, Šākir al-", "given": "Šākir", "family": "al- Faḥḥām", "initials": "Š. F."} {"name": "Federico Sánchez Bedoya", "given": "Federico", "middle": "Sánchez", "family": "Bedoya", "initials": "F. S. B."} {"name": "Feijoo y Montenegro, Benito Jerónimo", "given": "Benito", "middle": "Jerónimo", "family": "Feijoo y Montenegro", "initials": "B. J. F. M."} {"name": "Felip Lorda i Alaiz", "given": "Felip", "family": "Lorda i Alaiz", "initials": "F. L. A."} @@ -1158,7 +1158,7 @@ {"name": "Honorable Judge W. Harold Albritton, III", "title": "Honorable Judge", "given": "W.", "middle": "Harold", "family": "Albritton", "suffix": "III", "initials": "W. H. A."} {"name": "Honorable Terry F. Moorer", "title": "Honorable", "given": "Terry", "middle": "F.", "family": "Moorer", "initials": "T. F. M."} {"name": "Honorable W. Harold Albritton, III", "title": "Honorable", "given": "W.", "middle": "Harold", "family": "Albritton", "suffix": "III", "initials": "W. H. A."} -{"name": "Hooghvorst, Emmanuel d'", "given": "Emmanuel", "middle": "d'", "family": "Hooghvorst", "initials": "E. d. H."} +{"name": "Hooghvorst, Emmanuel d'", "given": "Emmanuel", "family": "d' Hooghvorst", "initials": "E. H."} {"name": "Horacio Gómez Bolaños", "given": "Horacio", "middle": "Gómez", "family": "Bolaños", "initials": "H. G. B."} {"name": "Horst, Jakob", "given": "Jakob", "family": "Horst", "initials": "J. H."} {"name": "Horvat, Irena", "given": "Irena", "family": "Horvat", "initials": "I. H."} @@ -1229,7 +1229,8 @@ {"name": "Ivan Tham Jun Hoe", "given": "Ivan", "middle": "Tham Jun", "family": "Hoe", "initials": "I. T. J. H."} {"name": "Ivanov Ivan", "given": "Ivanov", "family": "Ivan", "initials": "I. I."} {"name": "Ivanov Ivan Ivanovich", "given": "Ivanov", "middle": "Ivan", "family": "Ivanovich", "initials": "I. I. I."} -{"name": "Ivoi, Paul d'", "given": "Paul", "middle": "d'", "family": "Ivoi", "initials": "P. d. I."} +{"name": "Ivoi, Paul d'", "given": "Paul", "family": "d' Ivoi", "initials": "P. I."} +{"name": "Ivoi, d'", "family": "Ivoi d'", "initials": "I."} {"name": "Iván Duque Márquez", "given": "Iván", "middle": "Duque", "family": "Márquez", "initials": "I. D. M."} {"name": "Iñaki", "given": "Iñaki", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "I."} {"name": "Iñaki", "order": "FAMILY_FIRST", "family": "Iñaki", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "I."} @@ -2092,7 +2093,7 @@ {"name": "Manuel Velasco y Martínez", "given": "Manuel", "family": "Velasco y Martínez", "initials": "M. V. M."} {"name": "Manuela Lema Villar", "given": "Manuela", "middle": "Lema", "family": "Villar", "initials": "M. L. V."} {"name": "Mané Katy", "order": "FAMILY_FIRST", "given": "Katy", "family": "Mané", "initials": "K. M."} -{"name": "Maqrīzī, Aḥmad Ibn-ʿAlī al-", "given": "Aḥmad", "middle": "Ibn-ʿAlī al-", "family": "Maqrīzī", "initials": "A. I. a. M."} +{"name": "Maqrīzī, Aḥmad Ibn-ʿAlī al-", "given": "Aḥmad", "middle": "Ibn-ʿAlī", "family": "al- Maqrīzī", "initials": "A. I. M."} {"name": "Mar, Fien de la", "given": "Fien", "family": "de la Mar", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "F. M."} {"name": "Marc de Joybert", "given": "Marc", "family": "de Joybert", "initials": "M. J."} {"name": "Marco Antonio Flores", "given": "Marco", "middle": "Antonio", "family": "Flores", "initials": "M. A. F."} @@ -2185,7 +2186,7 @@ {"name": "Max Born", "given": "Max", "family": "Born", "initials": "M. B."} {"name": "Mayo", "given": "Mayo", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "M."} {"name": "Mañé i Miravet, Teresa", "given": "Teresa", "family": "Mañé i Miravet", "initials": "T. M. M."} -{"name": "Maǧrīṭī, Abu-'l-Qāsim Maslama Ibn-Aḥmad al-", "given": "Abu-'l-Qāsim", "middle": "Maslama Ibn-Aḥmad al-", "family": "Maǧrīṭī", "initials": "A. M. I. a. M."} +{"name": "Maǧrīṭī, Abu-'l-Qāsim Maslama Ibn-Aḥmad al-", "given": "Abu-'l-Qāsim", "middle": "Maslama Ibn-Aḥmad", "family": "al- Maǧrīṭī", "initials": "A. M. I. M."} {"name": "Mc Donald", "family": "Mc Donald", "initials": "D."} {"name": "Md Abdul Karim", "title": "Md", "given": "Abdul", "family": "Karim", "initials": "A. K."} {"name": "Md,, Phd", "family": "Md", "suffix": "Phd", "initials": "M."} @@ -2393,17 +2394,17 @@ {"name": "Orel, Irena", "given": "Irena", "family": "Orel", "initials": "I. O."} {"name": "Oren ben Horin", "given": "Oren", "middle": "ben", "family": "Horin", "initials": "O. b. H."} {"name": "Orlando Arvizu Lara", "given": "Orlando", "middle": "Arvizu", "family": "Lara", "initials": "O. A. L."} -{"name": "Orléans, Ferdinand-Philippe d'", "given": "Ferdinand-Philippe", "middle": "d'", "family": "Orléans", "initials": "F. d. O."} -{"name": "Orléans-Longueville, Jean Louis Charles d'", "given": "Jean", "middle": "Louis Charles d'", "family": "Orléans-Longueville", "initials": "J. L. C. d. O."} -{"name": "Orsi, Achille d'", "given": "Achille", "middle": "d'", "family": "Orsi", "initials": "A. d. O."} +{"name": "Orléans, Ferdinand-Philippe d'", "given": "Ferdinand-Philippe", "family": "d' Orléans", "initials": "F. O."} +{"name": "Orléans-Longueville, Jean Louis Charles d'", "given": "Jean", "middle": "Louis Charles", "family": "d' Orléans-Longueville", "initials": "J. L. C. O."} +{"name": "Orsi, Achille d'", "given": "Achille", "family": "d' Orsi", "initials": "A. O."} {"name": "Ortega y Gasset, Dr.", "title": "Dr.", "given": "Ortega", "middle": "y", "family": "Gasset", "initials": "O. y. G."} {"name": "Ortega y Gasset, Ed", "given": "Ortega", "middle": "y", "family": "Gasset", "suffix": "Ed", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "O. y. G."} {"name": "Orueta y Duarte, Ricardo de", "given": "Ricardo", "family": "de Orueta y Duarte", "initials": "R. O. D."} -{"name": "Orval, Pierre d'", "given": "Pierre", "middle": "d'", "family": "Orval", "initials": "P. d. O."} -{"name": "Orville, Johann Friedrich d'", "given": "Johann", "middle": "Friedrich d'", "family": "Orville", "initials": "J. F. d. O."} +{"name": "Orval, Pierre d'", "given": "Pierre", "family": "d' Orval", "initials": "P. O."} +{"name": "Orville, Johann Friedrich d'", "given": "Johann", "middle": "Friedrich", "family": "d' Orville", "initials": "J. F. O."} {"name": "Ospina Pérez, Mariano", "given": "Mariano", "family": "Ospina Pérez", "initials": "M. O. P."} {"name": "Otto III.", "given": "Otto", "suffix": "III.", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "O."} -{"name": "Outrein, Johannes d'", "given": "Johannes", "middle": "d'", "family": "Outrein", "initials": "J. d. O."} +{"name": "Outrein, Johannes d'", "given": "Johannes", "family": "d' Outrein", "initials": "J. O."} {"name": "Owura Bertram Wilberforce Henry Robert Wooster MP", "title": "Owura", "given": "Bertram", "middle": "Wilberforce Henry Robert", "family": "Wooster", "suffix": "MP", "initials": "B. W. H. R. W."} {"name": "Ozaki, Shirō", "given": "Shirō", "family": "Ozaki", "initials": "S. O."} {"name": "Ozawa, Seiji", "given": "Seiji", "family": "Ozawa", "initials": "S. O."} @@ -2558,6 +2559,7 @@ {"name": "Ramón Arellano Félix", "given": "Ramón", "middle": "Arellano", "family": "Félix", "initials": "R. A. F."} {"name": "Rascón Navarro Seña y Redondo, Juan Antonio", "given": "Juan", "middle": "Antonio", "family": "Rascón Navarro Seña y Redondo", "initials": "J. A. R. N. S. R."} {"name": "Rashid Maidin", "given": "Rashid", "family": "Maidin", "initials": "R. M."} +{"name": "Rashid, Harun al-", "given": "Harun", "family": "al- Rashid", "initials": "H. R."} {"name": "Razija Mujanović", "given": "Razija", "family": "Mujanović", "initials": "R. M."} {"name": "Reem Al-Bahrani, CPA", "given": "Reem", "family": "Al-Bahrani", "suffix": "CPA", "initials": "R. A."} {"name": "Reis, Maria Firmina dos", "given": "Maria", "middle": "Firmina", "family": "dos Reis", "initials": "M. F. R."} @@ -2579,7 +2581,7 @@ {"name": "Richard von Süßkind-Schwendi", "given": "Richard", "family": "von Süßkind-Schwendi", "initials": "R. S."} {"name": "Ridout, Alan", "given": "Alan", "family": "Ridout", "initials": "A. R."} {"name": "Riksdagsledamot Lars-Åke Karl Hermansson", "given": "Riksdagsledamot", "middle": "Lars-Åke Karl", "family": "Hermansson", "initials": "R. L. K. H."} -{"name": "Rikābī, Fuʾād ar-", "given": "Fuʾād", "middle": "ar-", "family": "Rikābī", "initials": "F. a. R."} +{"name": "Rikābī, Fuʾād ar-", "given": "Fuʾād", "family": "ar- Rikābī", "initials": "F. R."} {"name": "Rinpoche", "given": "Rinpoche", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "R."} {"name": "Rob And Beth Edmunds", "given": "Rob And Beth", "family": "Edmunds", "initials": "R. B. E."} {"name": "Rob Edmunds", "given": "Rob", "family": "Edmunds", "initials": "R. E."} @@ -2637,7 +2639,7 @@ {"name": "SMITH, VD DE LA MA", "family": "SMITH VD DE LA MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "S. M."} {"name": "SMITH, VD MA", "family": "SMITH VD MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "S. M."} {"name": "STEVEN HARDMAN, MD, DO, DDS", "given": "STEVEN", "family": "HARDMAN", "suffix": "MD, DO, DDS", "ambiguities": ["COMMA_STRUCTURE"], "initials": "S. H."} -{"name": "Saad, Habib Pacha es-", "given": "Habib", "middle": "Pacha es-", "family": "Saad", "initials": "H. P. e. S."} +{"name": "Saad, Habib Pacha es-", "given": "Habib", "middle": "Pacha", "family": "es- Saad", "initials": "H. P. S."} {"name": "Saar Samira Ahmet", "order": "FAMILY_FIRST", "given": "Samira", "middle": "Ahmet", "family": "Saar", "initials": "S. A. S."} {"name": "Sahagún, Bernardino de", "given": "Bernardino", "family": "de Sahagún", "initials": "B. S."} {"name": "Said oglu", "given": "Said", "family": "oglu", "initials": "S. o."} @@ -2974,7 +2976,7 @@ {"name": "Sölvi Ottesen", "given": "Sölvi", "family": "Ottesen", "initials": "S. O."} {"name": "Süßkind-Schwendi, Richard von", "given": "Richard", "family": "von Süßkind-Schwendi", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "R. S."} {"name": "Sābūr Ibn-Sahl", "given": "Sābūr", "family": "Ibn-Sahl", "initials": "S. I."} -{"name": "Sādāt, Ǧīhān as-", "given": "Ǧīhān", "middle": "as-", "family": "Sādāt", "initials": "Ǧ. a. S."} +{"name": "Sādāt, Ǧīhān as-", "given": "Ǧīhān", "family": "as- Sādāt", "initials": "Ǧ. S."} {"name": "Tadsch ad-Din al-Hasani", "given": "Tadsch", "middle": "ad-Din", "family": "al-Hasani", "initials": "T. a. a."} {"name": "Tahir Jalaluddin", "given": "Tahir", "family": "Jalaluddin", "initials": "T. J."} {"name": "Takamatsu, Jirō", "given": "Jirō", "family": "Takamatsu", "initials": "J. T."} @@ -3196,7 +3198,7 @@ {"name": "Yin a Le", "given": "Yin", "middle": "a", "family": "Le", "initials": "Y. a. L."} {"name": "Yoshi Emomoto", "given": "Yoshi", "family": "Emomoto", "initials": "Y. E."} {"name": "Yrsa Sigurðardóttir", "given": "Yrsa", "family": "Sigurðardóttir", "initials": "Y. S."} -{"name": "Yāzīǧī, Ibrāhīm al-", "given": "Ibrāhīm", "middle": "al-", "family": "Yāzīǧī", "initials": "I. a. Y."} +{"name": "Yāzīǧī, Ibrāhīm al-", "given": "Ibrāhīm", "family": "al- Yāzīǧī", "initials": "I. Y."} {"name": "Yıldırım, Fatma Su", "given": "Fatma", "middle": "Su", "family": "Yıldırım", "initials": "F. S. Y."} {"name": "Yılmaz Irmak", "order": "FAMILY_FIRST", "given": "Irmak", "family": "Yılmaz", "initials": "I. Y."} {"name": "Yılmaz, Irmak", "given": "Irmak", "family": "Yılmaz", "initials": "I. Y."} @@ -3210,7 +3212,7 @@ {"name": "Zaharias, Babe D.", "given": "Babe", "middle": "D.", "family": "Zaharias", "initials": "B. D. Z."} {"name": "Zaid Ibn-Ḥārīṯa", "given": "Zaid", "family": "Ibn-Ḥārīṯa", "initials": "Z. I."} {"name": "Zainab al-Ġazālī", "given": "Zainab", "family": "al-Ġazālī", "initials": "Z. a."} -{"name": "Zaiyāt, Laṭīfa az-", "given": "Laṭīfa", "middle": "az-", "family": "Zaiyāt", "initials": "L. a. Z."} +{"name": "Zaiyāt, Laṭīfa az-", "given": "Laṭīfa", "family": "az- Zaiyāt", "initials": "L. Z."} {"name": "Zalka, Máté", "given": "Máté", "family": "Zalka", "initials": "M. Z."} {"name": "Zamara Imani Bukenya", "given": "Zamara", "middle": "Imani", "family": "Bukenya", "initials": "Z. I. B."} {"name": "Zarathustra", "given": "Zarathustra", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "Z."} @@ -3218,7 +3220,7 @@ {"name": "Zazilia Hamish Stober", "given": "Zazilia", "middle": "Hamish", "family": "Stober", "initials": "Z. H. S."} {"name": "Zazilia Hamiş Ştöber", "given": "Zazilia", "middle": "Hamiş", "family": "Ştöber", "initials": "Z. H. Ş."} {"name": "Zaziliya Hemish Shtober", "given": "Zaziliya", "middle": "Hemish", "family": "Shtober", "initials": "Z. H. S."} -{"name": "Zaʿīm, Ḥusnī az-", "given": "Ḥusnī", "middle": "az-", "family": "Zaʿīm", "initials": "Ḥ. a. Z."} +{"name": "Zaʿīm, Ḥusnī az-", "given": "Ḥusnī", "family": "az- Zaʿīm", "initials": "Ḥ. Z."} {"name": "Zein El Dine", "given": "Zein", "middle": "El", "family": "Dine", "initials": "Z. E. D."} {"name": "Zelda Hamish Støver", "given": "Zelda", "middle": "Hamish", "family": "Støver", "initials": "Z. H. S."} {"name": "Zelda Herman Stander", "given": "Zelda", "middle": "Herman", "family": "Stander", "initials": "Z. H. S."} @@ -3575,14 +3577,14 @@ {"name": "Āqā Buzurg aṭ-Ṭihrānī", "given": "Āqā", "middle": "Buzurg", "family": "aṭ-Ṭihrānī", "initials": "Ā. B. a."} {"name": "ĐBQH Nguyễn Văn Anh", "order": "FAMILY_FIRST", "given": "Nguyễn", "middle": "Văn Anh", "family": "ĐBQH", "initials": "N. V. A. Đ."} {"name": "Ğawīdān _ Hānum", "given": "Ğawīdān", "middle": "_", "family": "Hānum", "initials": "Ğ. _. H."} -{"name": "Ġazālī, Zainab al-", "given": "Zainab", "middle": "al-", "family": "Ġazālī", "initials": "Z. a. Ġ."} +{"name": "Ġazālī, Zainab al-", "given": "Zainab", "family": "al- Ġazālī", "initials": "Z. Ġ."} {"name": "İren Adler", "given": "İren", "family": "Adler", "initials": "İ. A."} {"name": "Ōtani, Kōzui", "given": "Kōzui", "family": "Ōtani", "initials": "K. Ō."} {"name": "Ştöber Zazilia Hamiş", "order": "FAMILY_FIRST", "given": "Zazilia", "middle": "Hamiş", "family": "Ştöber", "initials": "Z. H. Ş."} -{"name": "Šaʿrāwī, Hudā aš-", "given": "Hudā", "middle": "aš-", "family": "Šaʿrāwī", "initials": "H. a. Š."} +{"name": "Šaʿrāwī, Hudā aš-", "given": "Hudā", "family": "aš- Šaʿrāwī", "initials": "H. Š."} {"name": "Šejc, Jan Měto", "given": "Jan", "middle": "Měto", "family": "Šejc", "initials": "J. M. Š."} {"name": "Šejc, Jan Měto", "order": "FAMILY_FIRST", "given": "Jan", "middle": "Měto", "family": "Šejc", "initials": "J. M. Š."} -{"name": "Šiblī, Abū-Bakr aš-", "given": "Abū-Bakr", "middle": "aš-", "family": "Šiblī", "initials": "A. a. Š."} +{"name": "Šiblī, Abū-Bakr aš-", "given": "Abū-Bakr", "family": "aš- Šiblī", "initials": "A. Š."} {"name": "Šihāb-ad-Dīn Muḥammad Ġūrī", "given": "Šihāb-ad-Dīn", "middle": "Muḥammad", "family": "Ġūrī", "initials": "Š. M. Ġ."} {"name": "Šołćina, Hanka", "given": "Hanka", "family": "Šołćina", "initials": "H. Š."} {"name": "Šołćina, Hanka", "order": "FAMILY_FIRST", "given": "Hanka", "family": "Šołćina", "initials": "H. Š."} @@ -3590,7 +3592,7 @@ {"name": "Šteber, Cecilija Hemiš", "given": "Cecilija", "middle": "Hemiš", "family": "Šteber", "initials": "C. H. Š."} {"name": "Šākir al-Faḥḥām", "given": "Šākir", "family": "al-Faḥḥām", "initials": "Š. a."} {"name": "Ǧalāl-ad-Dīn Rūmī", "given": "Ǧalāl-ad-Dīn", "family": "Rūmī", "initials": "Ǧ. R."} -{"name": "Ǧaʿfarī, Ibrāhīm al-", "given": "Ibrāhīm", "middle": "al-", "family": "Ǧaʿfarī", "initials": "I. a. Ǧ."} +{"name": "Ǧaʿfarī, Ibrāhīm al-", "given": "Ibrāhīm", "family": "al- Ǧaʿfarī", "initials": "I. Ǧ."} {"name": "Ǧīhān as-Sādāt", "given": "Ǧīhān", "family": "as-Sādāt", "initials": "Ǧ. a."} {"name": "ʊŋono Alaaji KALAM Alɛ Mustafa", "order": "FAMILY_FIRST", "given": "Alaaji", "middle": "KALAM Alɛ Mustafa", "family": "ʊŋono", "initials": "A. K. A. M. ʊ."} {"name": "ʊŋono Alaaji Mustafa KALAM Alɛ", "given": "ʊŋono", "middle": "Alaaji Mustafa KALAM", "family": "Alɛ", "initials": "ʊ. A. M. K. A."} @@ -5343,14 +5345,14 @@ {"name": "ស្តូបើ, ហ្សាហ្សីលៀ ហេមីស", "given": "ហ្សាហ្សីលៀ", "middle": "ហេមីស", "family": "ស្តូបើ", "initials": "ហ. ហ. ស."} {"name": "ហ្សាហ្សីលៀ ហេមីស ស្តូបើ", "given": "ហ្សាហ្សីលៀ", "middle": "ហេមីស", "family": "ស្តូបើ", "initials": "ហ. ហ. ស."} {"name": "Ḥabīballāh Kalakānī", "given": "Ḥabīballāh", "family": "Kalakānī", "initials": "Ḥ. K."} -{"name": "Ḥakīm, Taufīq al-", "given": "Taufīq", "middle": "al-", "family": "Ḥakīm", "initials": "T. a. Ḥ."} +{"name": "Ḥakīm, Taufīq al-", "given": "Taufīq", "family": "al- Ḥakīm", "initials": "T. Ḥ."} {"name": "Ḥamza Fanṣūrī", "given": "Ḥamza", "family": "Fanṣūrī", "initials": "Ḥ. F."} {"name": "Ḥusain Ṯanāʾī Mašhadī", "given": "Ḥusain", "middle": "Ṯanāʾī", "family": "Mašhadī", "initials": "Ḥ. Ṯ. M."} {"name": "Ḥusnī az-Zaʿīm", "given": "Ḥusnī", "family": "az-Zaʿīm", "initials": "Ḥ. a."} -{"name": "Ḥāǧǧ, Kamāl Yūsuf al-", "given": "Kamāl", "middle": "Yūsuf al-", "family": "Ḥāǧǧ", "initials": "K. Y. a. Ḥ."} -{"name": "Ḫarrāṭ, Idwār al-", "given": "Idwār", "middle": "al-", "family": "Ḫarrāṭ", "initials": "I. a. Ḫ."} +{"name": "Ḥāǧǧ, Kamāl Yūsuf al-", "given": "Kamāl", "middle": "Yūsuf", "family": "al- Ḥāǧǧ", "initials": "K. Y. Ḥ."} +{"name": "Ḫarrāṭ, Idwār al-", "given": "Idwār", "family": "al- Ḫarrāṭ", "initials": "I. Ḫ."} {"name": "Ṣafī-ad-Dīn Ardabīlī", "given": "Ṣafī-ad-Dīn", "family": "Ardabīlī", "initials": "Ṣ. A."} -{"name": "Ṭihrānī, Āqā Buzurg aṭ-", "given": "Āqā", "middle": "Buzurg aṭ-", "family": "Ṭihrānī", "initials": "Ā. B. a. Ṭ."} +{"name": "Ṭihrānī, Āqā Buzurg aṭ-", "given": "Āqā", "middle": "Buzurg", "family": "aṭ- Ṭihrānī", "initials": "Ā. B. Ṭ."} {"name": "Ṯābit Ibn-Qurra", "given": "Ṯābit", "family": "Ibn-Qurra", "initials": "Ṯ. I."} {"name": "‏John‏ Smith", "given": "John", "family": "Smith", "initials": "J. S."} {"name": "‏محمد بن سلمان‏", "given": "محمد", "family": "بن سلمان", "initials": "م. س."} From de6c045ddbe109f979644183deb33c6adbcef18b Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sat, 17 Oct 2026 17:45:17 -0700 Subject: [PATCH 2/5] Keep a marked particle lowercase in case repair; review fixes (#700) capitalized() split "d'" and "az-" at the mark and asked the particle test of the stem, so "IVOI, PAUL D'" repaired to "Paul D' Ivoi". The whole token is asked first now. Measured over every corpus name in three cases against 00dd2759: 29 names move, each only lowercasing a token ending in ' or -. From the design-docs review: add esh-, eth-, edh- (the a- series' ASCII digraphs had no e- twins); P6's sentence names the listed spellings and the never-given kind instead of "never a given name"; the "Ivoi, d'" boundary leaves P6, whose statement implies given d' (the case row keeps it); recompute recipe for the attested spellings; R4 examples for the repair; AGENTS.md records the render-path check. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- docs/design/decisions.md | 7 ++++--- docs/design/rules.md | 16 +++++++++------- nameparser/_render.py | 8 ++++++++ nameparser/config/particles.py | 2 +- tools/differential/corpus_rules.jsonl | 3 ++- tools/differential/snapshot.jsonl | 3 ++- 7 files changed, 27 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e74cc1cb..78e61b42 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -253,7 +253,7 @@ Add a dedicated `copy.deepcopy()` round-trip test for it too (see `test_regexes_ **Unknown-key attribute access on `TupleManager`/`RegexTupleManager` warns (1.4, #256) — 2.0 note: the warning became a hard `AttributeError` naming the known keys (shim `TupleManager`); the paragraph below describes the deleted v1 machinery and applies only when reading v1 history.** — a key not currently in the dict emits `DeprecationWarning` naming the miss and the known keys (`_warn_unknown_key`), before falling back to the same `None`/`EMPTY_REGEX` default as before; `.get()` stays silent. Dunder probes (`__deepcopy__`) still raise `AttributeError` outright, and single-underscore probes (`_repr_html_`, IPython's `_ipython_canary_method_should_not_exist_`, etc.) are excluded from the warning too — no real config key starts with `_`, so both guards just keep protocol/introspection probes from misfiring as "typo" warnings. This means internal parser code that reads `self.C.regexes.` unconditionally (e.g. `squash_bidi`'s `bidi`) now warns if a caller's custom `regexes` dict omits that key — a previously-silent partial-override pattern is on the same deprecation path as an actual typo. -**Adding a word to a config set** — first check the *other* sets for the same word (grep `nameparser/config/` or intersect the sets in a `python3 -c`). Real overlaps exist: `do`/`st`/`mc` ∈ `PARTICLES` ∩ `TITLES`/`SUFFIX_ACRONYMS`; `abd` = "ABD" ∈ `SUFFIX_ACRONYMS`; `abu` ∈ `PARTICLES` ∩ `BOUND_GIVEN_NAMES` (position-dependent: leading token → given-name join, mid-name → family-name join). Usually position-dependent and harmless, but can force a guard or an exclusion (the `last_base` all-particles guard; dropping `abd` from `BOUND_GIVEN_NAMES`). Then check the word against real names, not memory: `uv run python tools/name_usage.py ` counts its bearers as a given name and as a family name (US Census surnames, SSA given names, Wikidata with countries of citizenship). Pass a name as it is written (`McDonald`): Wikidata labels match exactly, and no rule rebuilds an internal capital from `mcdonald`. It reports and does not decide — read the counts against the position the set's claim acts on. And check the predicates that test a member's SPELLING rather than its membership: they encode what the set's members happened to look like when they were written. `_pieces.starts_a_credential_run` implemented rules.md#S2's "Latin" as `isascii()`, exact until #661 added `mlađi`, which would have read `mlađi Jones` differently from `stariji Jones`; the design-docs review caught it, after a first draft had credited the test to the wrong clause of S2 and proposed deleting it on a mutation that moved nothing because nothing pinned it. (#679 then dropped the script test on purpose, after measuring what it protected.) Before adding the first member of a new script, or the first accented one, grep the set's consumers for `isascii`, `isalpha` and script tests, and read the rule each one implements. +**Adding a word to a config set** — first check the *other* sets for the same word (grep `nameparser/config/` or intersect the sets in a `python3 -c`). Real overlaps exist: `do`/`st`/`mc` ∈ `PARTICLES` ∩ `TITLES`/`SUFFIX_ACRONYMS`; `abd` = "ABD" ∈ `SUFFIX_ACRONYMS`; `abu` ∈ `PARTICLES` ∩ `BOUND_GIVEN_NAMES` (position-dependent: leading token → given-name join, mid-name → family-name join). Usually position-dependent and harmless, but can force a guard or an exclusion (the `last_base` all-particles guard; dropping `abd` from `BOUND_GIVEN_NAMES`). Then check the word against real names, not memory: `uv run python tools/name_usage.py ` counts its bearers as a given name and as a family name (US Census surnames, SSA given names, Wikidata with countries of citizenship). Pass a name as it is written (`McDonald`): Wikidata labels match exactly, and no rule rebuilds an internal capital from `mcdonald`. It reports and does not decide — read the counts against the position the set's claim acts on. And check the predicates that test a member's SPELLING rather than its membership: they encode what the set's members happened to look like when they were written. `_pieces.starts_a_credential_run` implemented rules.md#S2's "Latin" as `isascii()`, exact until #661 added `mlađi`, which would have read `mlađi Jones` differently from `stariji Jones`; the design-docs review caught it, after a first draft had credited the test to the wrong clause of S2 and proposed deleting it on a mutation that moved nothing because nothing pinned it. (#679 then dropped the script test on purpose, after measuring what it protected.) Before adding the first member of a new script, or the first accented one, grep the set's consumers for `isascii`, `isalpha` and script tests, and read the rule each one implements. The same goes for the first member holding PUNCTUATION: #700 added `d'` and `az-`, the parse read them right and the snapshot was clean, and `capitalized()` still wrote `D' Ivoi`, because case repair splits a token at its apostrophe or hyphen and asks the vocabulary of the stem. The render views are invisible to the snapshot, so parse such a member's names through `capitalized()` and `initials()` in each case before calling it done. **Before adding a short/common word to `PARTICLES` globally**, test it mid-string against realistic 3-token names, not just check for English-word collisions: Korean/Vietnamese given names put a short syllable in the middle slot (`Park In Hwan`, `Nguyen To Nga`), and Western names put a bare initial there (`John V. Smith`). A word that looks safe ("nobody is named 'to'") can still swallow a real middle name/initial into the last name once it's a global prefix — confirmed regressions for `to`/`in`/`an`/`ten`/`then` and bare `v` this way (PR #191). diff --git a/docs/design/decisions.md b/docs/design/decisions.md index 4d3d5edb..b35f0409 100644 --- a/docs/design/decisions.md +++ b/docs/design/decisions.md @@ -172,9 +172,10 @@ Decided 2026-08-19 (was Open: [#380](https://github.com/derek73/python-nameparse NOT DECIDED HERE: the mixed-case `Doe, Jane PhD vd Ma` (middle 'vd Ma', silent), the issue's fourth bullet. Its reading is unchanged; whether the family-comma path should report the chain's fork there is a separate question. Open: #578 (decided the same day, the S2 section's #578 entry: silent, as it reads). Nor the positional read of a part before a comma followed by no name word, where the case still splits the word: `Smith vd Ma, Prof.` reads given 'Smith', family 'vd Ma', and `SMITH VD MA, PROF.` family 'SMITH', suffix 'VD MA', on this tree and on `e56dd296` alike — that part reads as the comma-less name does, and the comma-less name splits the same way. BLAST RADIUS, measured 2026-10-02 with `e56dd296` (the pre-change master) as the comparator. The gate's output is byte-identical at all five baselines apart from the tree path: no corpus name holds these shapes, the rules.md examples aside. Over a generated grid, 5,676 of 409,498 parses move, every one in one of two classes — 3,534 where a family gains the words behind its particle, 2,142 where `vd` or `mc` leaves the family for the credential run — and no list of ambiguity kinds changes; 1,310 of the movers are written in mixed case. Recompute: from the pool `vd mc de van do Ma MA PhD Jr John Berg V la Prof.`, build every `<1 or 2 words>, `, `<3 words>, <1 word>`, `Smith , John`, `Doe, Jane <2-4 words>` and `Smith, <2-4 words>`; deduplicate, then parse each as written, `.upper()` and `.lower()`, keyed by the resulting text; keep the entries whose `as_dict()` or sorted ambiguity kinds differ between the trees, and class each by whether the new family holds every old family word and more, or the old family lost only `vd`/`mc` while the suffix kept every word. Cost: the given-slot test was QUADRATIC as first written, rescanning the particle run once per member — the second review measured 29,670 → 165,591 frames on `SMITH, VD ` + `DE `×k + `MA `×k at k = 256 against `e56dd296`, where an earlier wall-clock check at k = 100 and 400 had read 3.18× and passed for linear. The run's end is now kept once per title set: 1.90–1.99× per doubling of k from 16 to 256, and `_REREAD_SHAPES["lead"]` in tests/v2/test_benchmark.py guards it (generator frames at 8 units against 32: 3.38× kept, 7.53× rescanning, bound 6.0). The frame-count reference band did not move. - 2026-10-06 #613 — THE COMMA SITE IS DECIDED IN THE PASS THAT READS THE GIVEN PART. #611 found P6 showing the decisive sign smaller: assign held back from #602's run exactly the particle tail post_rules would attach a stage later, by asking `particle_tail` over roles that meant "not yet read" there and "suffix" in post_rules. The attachment and its third exception (`_inside_a_credential_run`) moved into assign, right after the given-part walk, and `particle_tail` is asked ONCE, at the moment the walk reaches #602's run: the pieces in front of the run hold the walk's roles and the run's words none, the state in which its answer equals the one over the finished roles (argued piece by piece in its docstring). post_rules ran P1's leading fold before P6, so assign asks P1's condition itself (`is_lone_never_given_particle` on the given piece), which is what keeps `Smith, de Mesnil van` the fold's. H1 and M4, the other rules ahead of the old site, are gated on there being no family, which a family comma always has. The no-comma site (#467) stays in post_rules, since it runs over the order P1's family-first redistribution leaves. NOTHING MOVES, measured 2026-10-06 against the commit before it: the P6 grid (103,786 distinct texts: each of the heads `Berg, Jan`, `Berg, Jan Piet`, `BERG, JAN`, `Berg, Dr. Jan`, `Berg, de`, `Berg, van`, `Berg, abdul`, `Smith, de Mesnil`, `de la, Jan` and `Nguyen, Thi`, followed by every sequence of zero to three of `van de der vd VD mc do DO Do PhD Jr. MA Ma Prof. V la Piet`, with and without a trailing `, Jr.` -- under the default policy, `lenient_comma_suffixes=False`, a family-first order and `middle_as_family`; comparator every token's role and folded or unjoined tag, every ambiguity's kind, detail and token IN ORDER, and `initials()`) and the #613 harness's 80,423 texts are identical, the gate's output byte-identical at all five baselines but for the baseline path, and the suite unchanged. ONE SHAPE DID MOVE, and the PR review found it on a grid with an EMPTY part before the comma, a head no measurement above held: there no family is named, H1 and M4 are not gated off, and they had made the given word the family before the old site attached -- `, Mr. Jones vd` read title 'Mr.', family 'Jones', suffix 'vd', and assign attaching first read given 'Jones', family 'vd' (177 texts on that grid). Reading P6 strictly instead (no family named, no attachment) moved 1,302 texts against master, `, Jan van` among them, so the attachment behind an empty family part stays in post_rules, after H1 and M4, through the same helper; that grid now differs from b547c247 only in C1's decided classes. Frames drop on every family-comma name, post_rules' block having paid a second `particle_tail` and a generator-driven search for the given word on every one, tail or not, where assign now attaches only behind a non-empty tail: `Smith, John` 169 → 160, `Smith, John, Jr.` 213 → 204, `Doe, Jane nee Smith` 302 → 293, a no-comma name unchanged. -- 2026-10-17 #700 — THE ELIDED PARTICLE AND THE HYPHEN-ATTACHED ARABIC ARTICLE ARE PARTICLE VOCABULARY, NEVER-GIVEN, AND THE RULE IS UNCHANGED. Library catalogs file `Paul d'Ivoi` as `Ivoi, Paul d'` and `Ḥusnī az-Zaʿīm` as `Zaʿīm, Ḥusnī az-`, the particle split off with its mark still on it; both read as a middle name (1.4.0 and 2.4 alike) because neither written form was in `PARTICLES`. P6 asks only the particle tag, so the fix is the vocabulary alone: no code moved. Three decisions, each taken by Derek on the #700 session's options. ELIDED SET: `d'`, `l'`, `dell'`, each also in its U+2019 spelling, and `de’` beside the existing `de'`, since nothing folds the typographic apostrophe to the ASCII one. ARTICLES: a CLOSED list — `al-`, `ul-`, the article assimilated to each of the fourteen sun letters in ASCII and DMG spellings (`ar-`, `az-`, `aš-`, `ash-`, `aṭ-`, …), and the Egyptian `el-` with its assimilations (`es-`, `ed-`, …) — all never-given, where the bare `al` stays ambiguous (Al Gore): written apart with its hyphen, the word is the article and nothing else. RENDERING: the attached family joins with a space like every particle, `d' Ivoi` and `al- Rashid`. - Declined: a shape rule (a vowel, consonants, a hyphen) in place of the list. It would need a new predicate and the bare-word collision check the issue named, for a population the list already covers; the corpora attest seven article spellings (`al- az- aš- ar- es- as- aṭ-`), all in it. - Not decided here: the GLUED forms (`Paul d'Ivoi`, `Harun al-Rashid`) stay one word with no particle split, out of #700's scope by its own statement. Hyphen-minus only: U+2010 and the non-breaking hyphen are not listed. The Scottish Gaelic `a’` (`Mac a’ Ghobhainn`, in corpus_cldr.jsonl) is mid-name and not an index-form particle, and was not added. +- 2026-10-17 #700 — THE ELIDED PARTICLE AND THE HYPHEN-ATTACHED ARABIC ARTICLE ARE PARTICLE VOCABULARY, NEVER-GIVEN, AND THE RULE IS UNCHANGED. Library catalogs file `Paul d'Ivoi` as `Ivoi, Paul d'` and `Ḥusnī az-Zaʿīm` as `Zaʿīm, Ḥusnī az-`, the particle split off with its mark still on it; both read as a middle name (1.4.0 and 2.4 alike) because neither written form was in `PARTICLES`. P6 asks only the particle tag, so the fix is the vocabulary alone: no code moved. Three decisions, each taken by Derek on the #700 session's options. ELIDED SET: `d'`, `l'`, `dell'`, each also in its U+2019 spelling, and `de’` beside the existing `de'`, since nothing folds the typographic apostrophe to the ASCII one. ARTICLES: a CLOSED list — `al-`, `ul-`, the article assimilated to each of the fourteen sun letters in ASCII and DMG spellings (`ar-`, `az-`, `aš-`, `ash-`, `aṭ-`, …), and the Egyptian `el-` with its assimilations (`es-`, `esh-`, `ed-`, …) — all never-given, where the bare `al` stays ambiguous (Al Gore): written apart with its hyphen, the word is the article and nothing else. RENDERING: the attached family joins with a space like every particle, `d' Ivoi` and `al- Rashid`. + Declined: a shape rule (a vowel, consonants, a hyphen) in place of the list. It would need a new predicate and the bare-word collision check the issue named, for a population the list already covers; the corpora attest seven article spellings (`al- az- aš- ar- es- as- aṭ-`), all in it — recompute: every whitespace- or comma-bounded token of `tools/differential/corpus*.jsonl` matching `\w{1,4}-` at a name's end. + Not decided here: the GLUED forms (`Paul d'Ivoi`, `Harun al-Rashid`) stay one word with no particle split, out of #700's scope by its own statement. Hyphen-minus only: U+2010 and the non-breaking hyphen are not listed. The Scottish Gaelic `a’` (`Mac a’ Ghobhainn`, in corpus_cldr.jsonl) is mid-name and not an index-form particle, and was not added. Nor are the elisions no corpus holds (`dall'`, `nell'`, `degl'`): the list is closed, so such a spelling reads as a middle name until it is listed. + RENDERING FOLLOWED THE VOCABULARY, found in review: R4's `capitalized()` split a token at its mark and asked the particle test of the stem, so `IVOI, PAUL D'` repaired to `Paul D' Ivoi` — 30 of the 32 first-listed spellings, all but `al-` and `de’`, whose stems are particles themselves. The whole token is now asked first, so R4's "a particle is written in lowercase" holds for them. The snapshot cannot see a render view (decisions.md#R4); measured instead by `capitalized()` and `capitalized(force=True)` over every corpus name as written, lowercased and uppercased, against `00dd2759`: 29 names move, every move lowercasing one token ending in `'` or `-`. The boundary `Ivoi, d'` (family `Ivoi d'`, as `Jong, de` reads) was taken out of rules.md#P6, whose statement implies given `d'`; it stays pinned as a case row. Measured 2026-10-17 against the parent commit `b4a004c3`: 39 corpus names move, every one the same shape — a trailing `d'` or article leaves the middle name and joins the family (38 in corpus_gnd.jsonl, `Orval, Pierre d'` in corpus_conventions.jsonl), and no ambiguity kind changes. Recompute: `uv run python tools/differential/snapshot.py --since b4a004c3`. That is evidence about the corpus as much as the change: these are the comma-bearing names ending in one of the new forms, and GND's index forms are where they occur. ### P7 — a one-letter particle against the initial it spells diff --git a/docs/design/rules.md b/docs/design/rules.md index a3632ac9..09218a4f 100644 --- a/docs/design/rules.md +++ b/docs/design/rules.md @@ -950,16 +950,16 @@ P6. Rationale: a particle ending the name has nothing to link "DOE, JANE PHD VD" → family="VD DOE" · boundary "DOE, JANE MA VD PHD" → suffix="MA VD PHD" A particle an index form writes apart with its elision or its - hyphen still on it — the elided "d'" and the Arabic article in - its assimilated spellings, "al-" — is a particle like any other - here, and never a given name: written apart, the word is the - article and nothing else. It joins with a space like every - particle; written glued to the family ("d'Ivoi") it is one word - and this rule never sees it. + hyphen still on it is a particle like any other here, where the + vocabulary lists that spelling — the elided "d'" and the Arabic + article in its assimilated spellings, "al-" among them — and of + the never-given kind, since written apart the word is the article + and nothing else. It joins with a space like every particle; + written glued to the family ("d'Ivoi") it is one word and this + rule never sees it. "Ivoi, Paul d'" → family="d' Ivoi" "Rashid, Harun al-" → family="al- Rashid" "Zaʿīm, Ḥusnī az-" → family="az- Zaʿīm" - "Ivoi, d'" → family="Ivoi d'" · boundary Without a comma, a declared family-first order has named the family in the same way and the attachment fires there too — but only where the run ENDS the name and stands in a MIDDLE — the one @@ -2873,6 +2873,8 @@ R4. Rationale: case repair is a display concern, applied only on "ANH DO" → capitalized="Anh Do" "anh van do" → capitalized="Anh Van Do" "SEÁN Ó MURCHÚ" → capitalized="Seán Ó Murchú" + "IVOI, PAUL D'" → capitalized="Paul d' Ivoi" + "zaʿīm, ḥusnī az-" → capitalized="Ḥusnī az- Zaʿīm" "ina binti navalamar" → capitalized="Ina binti Navalamar" · boundary "john smith phd" → capitalized="John Smith PhD" "john smith ph.d." → capitalized="John Smith Ph.D." diff --git a/nameparser/_render.py b/nameparser/_render.py index df24df11..7b4753cb 100644 --- a/nameparser/_render.py +++ b/nameparser/_render.py @@ -554,6 +554,14 @@ def _cap_text(text: str, role: Role, tags: frozenset[str], def cap(word: str) -> str: return _cap_word(word, role, tags, lex) + # A particle written with its mark ("d'", "az-", #700) is one + # vocabulary word that the per-word walk below would split at the + # mark, asking the particle test of its stem ('d', 'az') instead -- + # so rules.md#R4's "a particle is written in lowercase" is asked of + # the whole token first. _cap_word's particle arm keeps every gate + # it has; where one declines, the word repairs as the walk's would. + if _normalize(text) in lex.particles: + return cap(text) if "-" not in text: return _sub_words(cap, text) parts = text.split("-") diff --git a/nameparser/config/particles.py b/nameparser/config/particles.py index efc378ef..4c80590c 100644 --- a/nameparser/config/particles.py +++ b/nameparser/config/particles.py @@ -91,7 +91,7 @@ 'al-', 'ul-', 'at-', 'aṯ-', 'ath-', 'ad-', 'aḏ-', 'adh-', 'ar-', 'az-', 'as-', 'aš-', 'ash-', 'aṣ-', 'aḍ-', 'aṭ-', 'aẓ-', 'an-', - 'el-', 'et-', 'ed-', 'er-', 'ez-', 'es-', 'en-', + 'el-', 'et-', 'eth-', 'ed-', 'edh-', 'er-', 'ez-', 'es-', 'esh-', 'en-', 'degli', # Italian "of the", masc. pl. 'dei', # Italian "of the", masc. pl. 'delle', # Italian "of the", fem. pl. diff --git a/tools/differential/corpus_rules.jsonl b/tools/differential/corpus_rules.jsonl index 868b6bdd..dca4c73c 100644 --- a/tools/differential/corpus_rules.jsonl +++ b/tools/differential/corpus_rules.jsonl @@ -123,8 +123,8 @@ "Her Royal Highness Princess Anne" "His Excellency Lord Duncan" "II Van Johnson" +"IVOI, PAUL D'" "Ivoi, Paul d'" -"Ivoi, d'" "J-E-P DUPONT" "J. Smith" "J. née Jones Smith V" @@ -541,6 +541,7 @@ "van der Berg, MA" "van der Berg, PhD" "wang meng" +"zaʿīm, ḥusnī az-" "Ó. Pérez" "Агриппина Младшая" "Ана Хорват старији Џонс" diff --git a/tools/differential/snapshot.jsonl b/tools/differential/snapshot.jsonl index 001e2973..40026cb4 100644 --- a/tools/differential/snapshot.jsonl +++ b/tools/differential/snapshot.jsonl @@ -1178,6 +1178,7 @@ {"name": "I don", "given": "I", "family": "don", "initials": "I. d."} {"name": "II Van Johnson", "given": "II", "family": "Van Johnson", "initials": "I. J."} {"name": "III van Gogh", "given": "III", "family": "van Gogh", "initials": "I. G."} +{"name": "IVOI, PAUL D'", "given": "PAUL", "family": "D' IVOI", "initials": "P. I."} {"name": "Ibn-Ṭufail, Muḥammad Ibn-ʿAbd-al-Malik", "given": "Muḥammad", "middle": "Ibn-ʿAbd-al-Malik", "family": "Ibn-Ṭufail", "initials": "M. I. I."} {"name": "Ibrāhīm Ibn-Sinān", "given": "Ibrāhīm", "family": "Ibn-Sinān", "initials": "I. I."} {"name": "Ibrāhīm al-Yāzīǧī", "given": "Ibrāhīm", "family": "al-Yāzīǧī", "initials": "I. a."} @@ -1230,7 +1231,6 @@ {"name": "Ivanov Ivan", "given": "Ivanov", "family": "Ivan", "initials": "I. I."} {"name": "Ivanov Ivan Ivanovich", "given": "Ivanov", "middle": "Ivan", "family": "Ivanovich", "initials": "I. I. I."} {"name": "Ivoi, Paul d'", "given": "Paul", "family": "d' Ivoi", "initials": "P. I."} -{"name": "Ivoi, d'", "family": "Ivoi d'", "initials": "I."} {"name": "Iván Duque Márquez", "given": "Iván", "middle": "Duque", "family": "Márquez", "initials": "I. D. M."} {"name": "Iñaki", "given": "Iñaki", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "I."} {"name": "Iñaki", "order": "FAMILY_FIRST", "family": "Iñaki", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "I."} @@ -3547,6 +3547,7 @@ {"name": "won Baar Dominique Goudiaby, Prof. Dr. Adama Coulibaly Camara Massamba MD DDS", "title": "Prof. Dr.", "given": "Adama", "middle": "Coulibaly Camara Massamba", "family": "won Baar Dominique Goudiaby", "suffix": "MD DDS", "initials": "A. C. C. M. w. B. D. G."} {"name": "xyz. John Smith", "title": "xyz.", "given": "John", "family": "Smith", "initials": "J. S."} {"name": "ylymlaryň doktory, professor Ada Kornelia Sezar Martin fon Brül Gonzales Domingo, hirurg-stomatolog lukman", "title": "professor", "given": "Ada", "middle": "Kornelia Sezar Martin fon Brül Gonzales Domingo", "family": "ylymlaryň doktory", "suffix": "hirurg-stomatolog lukman", "ambiguities": ["COMMA_STRUCTURE"], "initials": "A. K. S. M. f. B. G. D. y. d."} +{"name": "zaʿīm, ḥusnī az-", "given": "ḥusnī", "family": "az- zaʿīm", "initials": "ḥ. z."} {"name": "Ábrányi, Emil", "given": "Emil", "family": "Ábrányi", "initials": "E. Á."} {"name": "Ábrányi, Emil von", "given": "Emil", "family": "von Ábrányi", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "E. Á."} {"name": "Álvaro Londoño Aristizábal", "given": "Álvaro", "middle": "Londoño", "family": "Aristizábal", "initials": "Á. L. A."} From 2eade8dbc659b2e09f829cefe981391098940615 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sat, 17 Oct 2026 17:54:00 -0700 Subject: [PATCH 3/5] Lowercase a marked particle only where the particle arm would (#700) Second review of de6c045d: the early return sent every whole-token particle down _cap_word's remaining clauses where the particle arm declined, moving "smith, john, ed-" (ED- -> Ed-) and a configured "van-der" (Van-Der -> Van-der). The arm's test is now one shared predicate, _lowers_as_particle, and the early return takes only its lowercase; every decline goes to the per-word walk as before. The shipped 't is reached too: "van 't Hoff" repairs to 'van 't Hoff' where 1.4.0 through 2.4.2 wrote 'van 'T Hoff'. R4's reading; recorded with an R4 example, a release bullet and the decisions entry, whose render measurement is re-taken against 00dd2759 and names its corpus. Co-Authored-By: Claude Opus 5.5 --- docs/design/decisions.md | 2 +- docs/design/rules.md | 1 + docs/release_log.rst | 1 + nameparser/_render.py | 41 +++++++++++++++++---------- tools/differential/corpus_rules.jsonl | 1 + tools/differential/snapshot.jsonl | 1 + 6 files changed, 31 insertions(+), 16 deletions(-) diff --git a/docs/design/decisions.md b/docs/design/decisions.md index b35f0409..467a1bdb 100644 --- a/docs/design/decisions.md +++ b/docs/design/decisions.md @@ -175,7 +175,7 @@ Decided 2026-08-19 (was Open: [#380](https://github.com/derek73/python-nameparse - 2026-10-17 #700 — THE ELIDED PARTICLE AND THE HYPHEN-ATTACHED ARABIC ARTICLE ARE PARTICLE VOCABULARY, NEVER-GIVEN, AND THE RULE IS UNCHANGED. Library catalogs file `Paul d'Ivoi` as `Ivoi, Paul d'` and `Ḥusnī az-Zaʿīm` as `Zaʿīm, Ḥusnī az-`, the particle split off with its mark still on it; both read as a middle name (1.4.0 and 2.4 alike) because neither written form was in `PARTICLES`. P6 asks only the particle tag, so the fix is the vocabulary alone: no code moved. Three decisions, each taken by Derek on the #700 session's options. ELIDED SET: `d'`, `l'`, `dell'`, each also in its U+2019 spelling, and `de’` beside the existing `de'`, since nothing folds the typographic apostrophe to the ASCII one. ARTICLES: a CLOSED list — `al-`, `ul-`, the article assimilated to each of the fourteen sun letters in ASCII and DMG spellings (`ar-`, `az-`, `aš-`, `ash-`, `aṭ-`, …), and the Egyptian `el-` with its assimilations (`es-`, `esh-`, `ed-`, …) — all never-given, where the bare `al` stays ambiguous (Al Gore): written apart with its hyphen, the word is the article and nothing else. RENDERING: the attached family joins with a space like every particle, `d' Ivoi` and `al- Rashid`. Declined: a shape rule (a vowel, consonants, a hyphen) in place of the list. It would need a new predicate and the bare-word collision check the issue named, for a population the list already covers; the corpora attest seven article spellings (`al- az- aš- ar- es- as- aṭ-`), all in it — recompute: every whitespace- or comma-bounded token of `tools/differential/corpus*.jsonl` matching `\w{1,4}-` at a name's end. Not decided here: the GLUED forms (`Paul d'Ivoi`, `Harun al-Rashid`) stay one word with no particle split, out of #700's scope by its own statement. Hyphen-minus only: U+2010 and the non-breaking hyphen are not listed. The Scottish Gaelic `a’` (`Mac a’ Ghobhainn`, in corpus_cldr.jsonl) is mid-name and not an index-form particle, and was not added. Nor are the elisions no corpus holds (`dall'`, `nell'`, `degl'`): the list is closed, so such a spelling reads as a middle name until it is listed. - RENDERING FOLLOWED THE VOCABULARY, found in review: R4's `capitalized()` split a token at its mark and asked the particle test of the stem, so `IVOI, PAUL D'` repaired to `Paul D' Ivoi` — 30 of the 32 first-listed spellings, all but `al-` and `de’`, whose stems are particles themselves. The whole token is now asked first, so R4's "a particle is written in lowercase" holds for them. The snapshot cannot see a render view (decisions.md#R4); measured instead by `capitalized()` and `capitalized(force=True)` over every corpus name as written, lowercased and uppercased, against `00dd2759`: 29 names move, every move lowercasing one token ending in `'` or `-`. The boundary `Ivoi, d'` (family `Ivoi d'`, as `Jong, de` reads) was taken out of rules.md#P6, whose statement implies given `d'`; it stays pinned as a case row. + RENDERING FOLLOWED THE VOCABULARY, found in review: R4's `capitalized()` split a token at its mark and asked the particle test of the stem, so `IVOI, PAUL D'` repaired to `Paul D' Ivoi` — 30 of the 32 first-listed spellings, all but `al-` and `de’`, whose stems are particles themselves. The whole token is now asked the particle arm's question first, and lowercased only where that arm would lowercase it; wherever the arm declines (an all-particle part, the given or suffix role, a recorded casing) the token goes to the per-word walk as before — a first version sent every whole-token particle down the arm's other clauses instead, which moved `smith, john, ed-` from 'ED-' to 'Ed-' and a configured `van-der` from 'Van-Der' to 'Van-der' (the fix commit's own review). The one shipped word outside #700 it reaches is `'t`, whose stem is `t`: `van 't Hoff` repairs to 'van 't Hoff', where 1.4.0 through 2.4.2 wrote 'van 'T Hoff' — R4's reading, and the right Dutch one. A casing configured for a stem (`capitalization_exceptions` `al` → `AL`) no longer reaches the marked spelling, which is now a particle in its own right: `al-` is lowercased. The snapshot cannot see a render view (decisions.md#R4); measured instead by `capitalized()` and `capitalized(force=True)` against `00dd2759`, over every name in `de6c045d`'s `tools/differential/corpus*.jsonl` as written, lowercased and uppercased (28 names move, every move lowercasing one token ending in `'` or `-`), and over every particle in eight positions under all three orders (adding `'T` → `'t`, which no corpus name held until R4's `van 't hoff` example joined corpus_rules.jsonl). The boundary `Ivoi, d'` (family `Ivoi d'`, as `Jong, de` reads) was taken out of rules.md#P6, whose statement implies given `d'`; it stays pinned as a case row. Measured 2026-10-17 against the parent commit `b4a004c3`: 39 corpus names move, every one the same shape — a trailing `d'` or article leaves the middle name and joins the family (38 in corpus_gnd.jsonl, `Orval, Pierre d'` in corpus_conventions.jsonl), and no ambiguity kind changes. Recompute: `uv run python tools/differential/snapshot.py --since b4a004c3`. That is evidence about the corpus as much as the change: these are the comma-bearing names ending in one of the new forms, and GND's index forms are where they occur. ### P7 — a one-letter particle against the initial it spells diff --git a/docs/design/rules.md b/docs/design/rules.md index 09218a4f..46a9f780 100644 --- a/docs/design/rules.md +++ b/docs/design/rules.md @@ -2875,6 +2875,7 @@ R4. Rationale: case repair is a display concern, applied only on "SEÁN Ó MURCHÚ" → capitalized="Seán Ó Murchú" "IVOI, PAUL D'" → capitalized="Paul d' Ivoi" "zaʿīm, ḥusnī az-" → capitalized="Ḥusnī az- Zaʿīm" + "jacobus henricus van 't hoff" → capitalized="Jacobus Henricus van 't Hoff" "ina binti navalamar" → capitalized="Ina binti Navalamar" · boundary "john smith phd" → capitalized="John Smith PhD" "john smith ph.d." → capitalized="John Smith Ph.D." diff --git a/docs/release_log.rst b/docs/release_log.rst index 73cbc5fd..cf4e6a15 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -12,6 +12,7 @@ Release Log - **Add the Akan and Indonesian salutations owura and bapak as titles.** ``Bapak Dwi Putro`` gives title ``Bapak``, first ``Dwi``, last ``Putro``, where 1.4.0 through 2.4.2 gave first ``Bapak``, middle ``Dwi``. :issue:`662` - **Fix a title written before a family comma reading as part of the family name.** ``Dr. Smith, John`` gives title ``Dr.``, last ``Smith``, where 1.4.0 through 2.4.2 gave last ``Dr. Smith``. Only listed titles are read there, so ``Xyz. Smith, John`` keeps its family, and a title that is also a surname particle, such as ``St`` or ``Freiherr``, stays in the surname. :issue:`701` - **Fix a surname particle such as d' or al- reading as a middle name when a listing puts it after the given name.** ``Ivoi, Paul d'`` gives last ``d' Ivoi``, where 1.4.0 through 2.4.2 gave middle ``d'``; ``Zaʿīm, Ḥusnī az-`` gives last ``az- Zaʿīm``, and the Arabic article reads the same in its other assimilated spellings. A particle written joined to the surname, as in ``Paul d'Ivoi``, is unchanged. :issue:`700` + - **Fix case repair capitalizing the Dutch particle 't.** ``jacobus henricus van 't hoff`` repairs to ``Jacobus Henricus van 't Hoff``, where 1.4.0 through 2.4.2 gave ``van 'T Hoff``. :issue:`700` - **Add Gov as a title.** ``Gov Smith`` gives title ``Gov``, last ``Smith``, where 1.4.0 through 2.4.2 gave first ``Gov``, and ``Lt. Gov. Smith, John`` gives title ``Lt. Gov.`` with the change above. :issue:`701` * 2.4.2 - October 13, 2026 diff --git a/nameparser/_render.py b/nameparser/_render.py index 7b4753cb..f4856dac 100644 --- a/nameparser/_render.py +++ b/nameparser/_render.py @@ -337,6 +337,22 @@ def _apply_mask(word: str, mask: str) -> str | None: return "".join(out) +def _lowers_as_particle(normalized: str, word: str, role: Role, + tags: frozenset[str], lex: Lexicon) -> bool: + """_cap_word's particle arm: `word` (folded to `normalized`) is a + particle doing a particle's work, so it is written in lowercase. + Shared with _cap_text, which asks it of a whole marked token + before splitting the token into words (#700).""" + return (normalized in lex.particles + and not _spells_an_initial(normalized, word) + and role in (Role.MIDDLE, Role.FAMILY) + and UNJOINED_TAG not in tags + # rules.md#R4: "a particle the vocabulary records a + # casing for takes that casing" -- the mask below + # outranks the particle's lowercase (#604) + and normalized not in lex.capitalization_exceptions_map) + + def _cap_word(word: str, role: Role, tags: frozenset[str], lex: Lexicon) -> str: # Clause order: the particle/connective arm, then the exceptions @@ -441,14 +457,7 @@ def _cap_word(word: str, role: Role, tags: frozenset[str], # the same words parsed gave 'E-F' (#463 review). generation = role is Role.SUFFIX and "vocab:suffix" in tags if not generation and ( - (normalized in lex.particles - and not _spells_an_initial(normalized, word) - and role in (Role.MIDDLE, Role.FAMILY) - and UNJOINED_TAG not in tags - # rules.md#R4: "a particle the vocabulary records a - # casing for takes that casing" -- the mask below - # outranks the particle's lowercase (#604) - and normalized not in lex.capitalization_exceptions_map) + _lowers_as_particle(normalized, word, role, tags, lex) or "conjunction" in tags or (UNCLASSIFIED_TAG in tags and _reads_as_conjunction(word, lex))): @@ -554,14 +563,16 @@ def _cap_text(text: str, role: Role, tags: frozenset[str], def cap(word: str) -> str: return _cap_word(word, role, tags, lex) - # A particle written with its mark ("d'", "az-", #700) is one + # A particle written with a mark ("d'", "az-", "'t", #700) is one # vocabulary word that the per-word walk below would split at the - # mark, asking the particle test of its stem ('d', 'az') instead -- - # so rules.md#R4's "a particle is written in lowercase" is asked of - # the whole token first. _cap_word's particle arm keeps every gate - # it has; where one declines, the word repairs as the walk's would. - if _normalize(text) in lex.particles: - return cap(text) + # mark, asking the particle test of its stem ('d', 'az', 't') + # instead -- so rules.md#R4's "a particle is written in lowercase" + # is asked of the whole token first. Only the lowercase is taken + # here; wherever the arm declines (an all-particle part, the given + # or suffix role, a recorded casing) the token goes to the walk + # exactly as before. + if _lowers_as_particle(_normalize(text), text, role, tags, lex): + return text.lower() if "-" not in text: return _sub_words(cap, text) parts = text.split("-") diff --git a/tools/differential/corpus_rules.jsonl b/tools/differential/corpus_rules.jsonl index dca4c73c..4a2011ba 100644 --- a/tools/differential/corpus_rules.jsonl +++ b/tools/differential/corpus_rules.jsonl @@ -491,6 +491,7 @@ "ibn Awf abdul Rahman" "ina binti navalamar" "j.-e.-p. dupont" +"jacobus henricus van 't hoff" "john doe q.c." "john e smith" "john smith EdD" diff --git a/tools/differential/snapshot.jsonl b/tools/differential/snapshot.jsonl index 40026cb4..fec30f04 100644 --- a/tools/differential/snapshot.jsonl +++ b/tools/differential/snapshot.jsonl @@ -3356,6 +3356,7 @@ {"name": "ina binti navalamar", "given": "ina", "family": "binti navalamar", "initials": "i. n."} {"name": "j.-e.-p. dupont", "given": "j.-e.-p.", "family": "dupont", "initials": "j. d."} {"name": "jack ma", "given": "jack", "family": "ma", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "j. m."} +{"name": "jacobus henricus van 't hoff", "given": "jacobus", "middle": "henricus", "family": "van 't hoff", "ambiguities": ["UNBALANCED_DELIMITER"], "initials": "j. h. h."} {"name": "jane doe nee smith ma", "given": "jane", "family": "doe", "suffix": "ma", "maiden": "smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "j. d."} {"name": "jenap Atajan Saparowiç Gurdow magistr, ylymlaryň kandidaty", "given": "ylymlaryň", "middle": "kandidaty", "family": "jenap Atajan Saparowiç Gurdow magistr", "initials": "y. k. j. A. S. G. m."} {"name": "joao da silva do amaral de souza", "given": "joao", "middle": "da silva do amaral", "family": "de souza", "initials": "j. s. a. s."} From 345ff5347fc2c114afcc0960d6fd73d7490105e1 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sat, 17 Oct 2026 18:11:22 -0700 Subject: [PATCH 4/5] =?UTF-8?q?Address=20PR=20review:=20decline-path=20tes?= =?UTF-8?q?t,=20=E2=80=99t=20twin,=20row=20labels,=20prose=20(#700)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test_render: pin that a marked particle lowercases only where the particle arm would. The all-particle part, the declared given slot, the suffix ed- and a configured van-der keep the walk's reading. Measured: the first broad early return fails it, where it had passed the whole suite. - particles.py: add ’t beside 't, and assert at import that every elided entry has its ASCII and its U+2019 spelling. - cases.py: "Ivoi, d'" was labelled parity by default, but 1.4.0 through 2.4.2 read given d'. Relabel it fix(#700). Add rows for the empty head, a particle-surname head, a glued comma, and ’t. - Prose: AGENTS.md no longer says initials() is invisible to the snapshot. Corrected the d' comment, the sun-letter count (lām's form is al- itself) and the ASCII-only el- series. The decisions entry now explains why the boundary left P6. The release bullet carries one before/after example. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- docs/design/decisions.md | 4 ++-- docs/design/rules.md | 3 ++- docs/release_log.rst | 2 +- nameparser/_render.py | 11 +++++---- nameparser/config/particles.py | 13 ++++++++--- tests/v2/cases.py | 33 ++++++++++++++++++++++++-- tests/v2/test_render.py | 42 +++++++++++++++++++++++++++++++++- 8 files changed, 94 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 78e61b42..86e451b2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -253,7 +253,7 @@ Add a dedicated `copy.deepcopy()` round-trip test for it too (see `test_regexes_ **Unknown-key attribute access on `TupleManager`/`RegexTupleManager` warns (1.4, #256) — 2.0 note: the warning became a hard `AttributeError` naming the known keys (shim `TupleManager`); the paragraph below describes the deleted v1 machinery and applies only when reading v1 history.** — a key not currently in the dict emits `DeprecationWarning` naming the miss and the known keys (`_warn_unknown_key`), before falling back to the same `None`/`EMPTY_REGEX` default as before; `.get()` stays silent. Dunder probes (`__deepcopy__`) still raise `AttributeError` outright, and single-underscore probes (`_repr_html_`, IPython's `_ipython_canary_method_should_not_exist_`, etc.) are excluded from the warning too — no real config key starts with `_`, so both guards just keep protocol/introspection probes from misfiring as "typo" warnings. This means internal parser code that reads `self.C.regexes.` unconditionally (e.g. `squash_bidi`'s `bidi`) now warns if a caller's custom `regexes` dict omits that key — a previously-silent partial-override pattern is on the same deprecation path as an actual typo. -**Adding a word to a config set** — first check the *other* sets for the same word (grep `nameparser/config/` or intersect the sets in a `python3 -c`). Real overlaps exist: `do`/`st`/`mc` ∈ `PARTICLES` ∩ `TITLES`/`SUFFIX_ACRONYMS`; `abd` = "ABD" ∈ `SUFFIX_ACRONYMS`; `abu` ∈ `PARTICLES` ∩ `BOUND_GIVEN_NAMES` (position-dependent: leading token → given-name join, mid-name → family-name join). Usually position-dependent and harmless, but can force a guard or an exclusion (the `last_base` all-particles guard; dropping `abd` from `BOUND_GIVEN_NAMES`). Then check the word against real names, not memory: `uv run python tools/name_usage.py ` counts its bearers as a given name and as a family name (US Census surnames, SSA given names, Wikidata with countries of citizenship). Pass a name as it is written (`McDonald`): Wikidata labels match exactly, and no rule rebuilds an internal capital from `mcdonald`. It reports and does not decide — read the counts against the position the set's claim acts on. And check the predicates that test a member's SPELLING rather than its membership: they encode what the set's members happened to look like when they were written. `_pieces.starts_a_credential_run` implemented rules.md#S2's "Latin" as `isascii()`, exact until #661 added `mlađi`, which would have read `mlađi Jones` differently from `stariji Jones`; the design-docs review caught it, after a first draft had credited the test to the wrong clause of S2 and proposed deleting it on a mutation that moved nothing because nothing pinned it. (#679 then dropped the script test on purpose, after measuring what it protected.) Before adding the first member of a new script, or the first accented one, grep the set's consumers for `isascii`, `isalpha` and script tests, and read the rule each one implements. The same goes for the first member holding PUNCTUATION: #700 added `d'` and `az-`, the parse read them right and the snapshot was clean, and `capitalized()` still wrote `D' Ivoi`, because case repair splits a token at its apostrophe or hyphen and asks the vocabulary of the stem. The render views are invisible to the snapshot, so parse such a member's names through `capitalized()` and `initials()` in each case before calling it done. +**Adding a word to a config set** — first check the *other* sets for the same word (grep `nameparser/config/` or intersect the sets in a `python3 -c`). Real overlaps exist: `do`/`st`/`mc` ∈ `PARTICLES` ∩ `TITLES`/`SUFFIX_ACRONYMS`; `abd` = "ABD" ∈ `SUFFIX_ACRONYMS`; `abu` ∈ `PARTICLES` ∩ `BOUND_GIVEN_NAMES` (position-dependent: leading token → given-name join, mid-name → family-name join). Usually position-dependent and harmless, but can force a guard or an exclusion (the `last_base` all-particles guard; dropping `abd` from `BOUND_GIVEN_NAMES`). Then check the word against real names, not memory: `uv run python tools/name_usage.py ` counts its bearers as a given name and as a family name (US Census surnames, SSA given names, Wikidata with countries of citizenship). Pass a name as it is written (`McDonald`): Wikidata labels match exactly, and no rule rebuilds an internal capital from `mcdonald`. It reports and does not decide — read the counts against the position the set's claim acts on. And check the predicates that test a member's SPELLING rather than its membership: they encode what the set's members happened to look like when they were written. `_pieces.starts_a_credential_run` implemented rules.md#S2's "Latin" as `isascii()`, exact until #661 added `mlađi`, which would have read `mlađi Jones` differently from `stariji Jones`; the design-docs review caught it, after a first draft had credited the test to the wrong clause of S2 and proposed deleting it on a mutation that moved nothing because nothing pinned it. (#679 then dropped the script test on purpose, after measuring what it protected.) Before adding the first member of a new script, or the first accented one, grep the set's consumers for `isascii`, `isalpha` and script tests, and read the rule each one implements. The same goes for the first member holding PUNCTUATION: #700 added `d'` and `az-`, the parse read them right and every snapshot move was an intended one, and `capitalized()` still wrote `D' Ivoi`, because case repair splits a token at its apostrophe or hyphen and asks the vocabulary of the stem. `capitalized()` is invisible to the snapshot (`initials()` is recorded there), so parse such a member's names through `capitalized()` and `capitalized(force=True)` in each case before calling it done. **Before adding a short/common word to `PARTICLES` globally**, test it mid-string against realistic 3-token names, not just check for English-word collisions: Korean/Vietnamese given names put a short syllable in the middle slot (`Park In Hwan`, `Nguyen To Nga`), and Western names put a bare initial there (`John V. Smith`). A word that looks safe ("nobody is named 'to'") can still swallow a real middle name/initial into the last name once it's a global prefix — confirmed regressions for `to`/`in`/`an`/`ten`/`then` and bare `v` this way (PR #191). diff --git a/docs/design/decisions.md b/docs/design/decisions.md index 467a1bdb..7c4680e7 100644 --- a/docs/design/decisions.md +++ b/docs/design/decisions.md @@ -172,10 +172,10 @@ Decided 2026-08-19 (was Open: [#380](https://github.com/derek73/python-nameparse NOT DECIDED HERE: the mixed-case `Doe, Jane PhD vd Ma` (middle 'vd Ma', silent), the issue's fourth bullet. Its reading is unchanged; whether the family-comma path should report the chain's fork there is a separate question. Open: #578 (decided the same day, the S2 section's #578 entry: silent, as it reads). Nor the positional read of a part before a comma followed by no name word, where the case still splits the word: `Smith vd Ma, Prof.` reads given 'Smith', family 'vd Ma', and `SMITH VD MA, PROF.` family 'SMITH', suffix 'VD MA', on this tree and on `e56dd296` alike — that part reads as the comma-less name does, and the comma-less name splits the same way. BLAST RADIUS, measured 2026-10-02 with `e56dd296` (the pre-change master) as the comparator. The gate's output is byte-identical at all five baselines apart from the tree path: no corpus name holds these shapes, the rules.md examples aside. Over a generated grid, 5,676 of 409,498 parses move, every one in one of two classes — 3,534 where a family gains the words behind its particle, 2,142 where `vd` or `mc` leaves the family for the credential run — and no list of ambiguity kinds changes; 1,310 of the movers are written in mixed case. Recompute: from the pool `vd mc de van do Ma MA PhD Jr John Berg V la Prof.`, build every `<1 or 2 words>, `, `<3 words>, <1 word>`, `Smith , John`, `Doe, Jane <2-4 words>` and `Smith, <2-4 words>`; deduplicate, then parse each as written, `.upper()` and `.lower()`, keyed by the resulting text; keep the entries whose `as_dict()` or sorted ambiguity kinds differ between the trees, and class each by whether the new family holds every old family word and more, or the old family lost only `vd`/`mc` while the suffix kept every word. Cost: the given-slot test was QUADRATIC as first written, rescanning the particle run once per member — the second review measured 29,670 → 165,591 frames on `SMITH, VD ` + `DE `×k + `MA `×k at k = 256 against `e56dd296`, where an earlier wall-clock check at k = 100 and 400 had read 3.18× and passed for linear. The run's end is now kept once per title set: 1.90–1.99× per doubling of k from 16 to 256, and `_REREAD_SHAPES["lead"]` in tests/v2/test_benchmark.py guards it (generator frames at 8 units against 32: 3.38× kept, 7.53× rescanning, bound 6.0). The frame-count reference band did not move. - 2026-10-06 #613 — THE COMMA SITE IS DECIDED IN THE PASS THAT READS THE GIVEN PART. #611 found P6 showing the decisive sign smaller: assign held back from #602's run exactly the particle tail post_rules would attach a stage later, by asking `particle_tail` over roles that meant "not yet read" there and "suffix" in post_rules. The attachment and its third exception (`_inside_a_credential_run`) moved into assign, right after the given-part walk, and `particle_tail` is asked ONCE, at the moment the walk reaches #602's run: the pieces in front of the run hold the walk's roles and the run's words none, the state in which its answer equals the one over the finished roles (argued piece by piece in its docstring). post_rules ran P1's leading fold before P6, so assign asks P1's condition itself (`is_lone_never_given_particle` on the given piece), which is what keeps `Smith, de Mesnil van` the fold's. H1 and M4, the other rules ahead of the old site, are gated on there being no family, which a family comma always has. The no-comma site (#467) stays in post_rules, since it runs over the order P1's family-first redistribution leaves. NOTHING MOVES, measured 2026-10-06 against the commit before it: the P6 grid (103,786 distinct texts: each of the heads `Berg, Jan`, `Berg, Jan Piet`, `BERG, JAN`, `Berg, Dr. Jan`, `Berg, de`, `Berg, van`, `Berg, abdul`, `Smith, de Mesnil`, `de la, Jan` and `Nguyen, Thi`, followed by every sequence of zero to three of `van de der vd VD mc do DO Do PhD Jr. MA Ma Prof. V la Piet`, with and without a trailing `, Jr.` -- under the default policy, `lenient_comma_suffixes=False`, a family-first order and `middle_as_family`; comparator every token's role and folded or unjoined tag, every ambiguity's kind, detail and token IN ORDER, and `initials()`) and the #613 harness's 80,423 texts are identical, the gate's output byte-identical at all five baselines but for the baseline path, and the suite unchanged. ONE SHAPE DID MOVE, and the PR review found it on a grid with an EMPTY part before the comma, a head no measurement above held: there no family is named, H1 and M4 are not gated off, and they had made the given word the family before the old site attached -- `, Mr. Jones vd` read title 'Mr.', family 'Jones', suffix 'vd', and assign attaching first read given 'Jones', family 'vd' (177 texts on that grid). Reading P6 strictly instead (no family named, no attachment) moved 1,302 texts against master, `, Jan van` among them, so the attachment behind an empty family part stays in post_rules, after H1 and M4, through the same helper; that grid now differs from b547c247 only in C1's decided classes. Frames drop on every family-comma name, post_rules' block having paid a second `particle_tail` and a generator-driven search for the given word on every one, tail or not, where assign now attaches only behind a non-empty tail: `Smith, John` 169 → 160, `Smith, John, Jr.` 213 → 204, `Doe, Jane nee Smith` 302 → 293, a no-comma name unchanged. -- 2026-10-17 #700 — THE ELIDED PARTICLE AND THE HYPHEN-ATTACHED ARABIC ARTICLE ARE PARTICLE VOCABULARY, NEVER-GIVEN, AND THE RULE IS UNCHANGED. Library catalogs file `Paul d'Ivoi` as `Ivoi, Paul d'` and `Ḥusnī az-Zaʿīm` as `Zaʿīm, Ḥusnī az-`, the particle split off with its mark still on it; both read as a middle name (1.4.0 and 2.4 alike) because neither written form was in `PARTICLES`. P6 asks only the particle tag, so the fix is the vocabulary alone: no code moved. Three decisions, each taken by Derek on the #700 session's options. ELIDED SET: `d'`, `l'`, `dell'`, each also in its U+2019 spelling, and `de’` beside the existing `de'`, since nothing folds the typographic apostrophe to the ASCII one. ARTICLES: a CLOSED list — `al-`, `ul-`, the article assimilated to each of the fourteen sun letters in ASCII and DMG spellings (`ar-`, `az-`, `aš-`, `ash-`, `aṭ-`, …), and the Egyptian `el-` with its assimilations (`es-`, `esh-`, `ed-`, …) — all never-given, where the bare `al` stays ambiguous (Al Gore): written apart with its hyphen, the word is the article and nothing else. RENDERING: the attached family joins with a space like every particle, `d' Ivoi` and `al- Rashid`. +- 2026-10-17 #700 — THE ELIDED PARTICLE AND THE HYPHEN-ATTACHED ARABIC ARTICLE ARE PARTICLE VOCABULARY, NEVER-GIVEN, AND THE RULE IS UNCHANGED. Library catalogs file `Paul d'Ivoi` as `Ivoi, Paul d'` and `Ḥusnī az-Zaʿīm` as `Zaʿīm, Ḥusnī az-`, the particle split off with its mark still on it; both read as a middle name (1.4.0 and 2.4 alike) because neither written form was in `PARTICLES`. P6 asks only the particle tag, so the fix is the vocabulary alone: no code moved. Three decisions, each taken by Derek on the #700 session's options. ELIDED SET: `d'`, `l'`, `dell'`, each also in its U+2019 spelling, and `de’` and `’t` beside the existing `de'` and `'t` (`’t` added in the PR review, a gap the test review found), since nothing folds the typographic apostrophe to the ASCII one; particles.py asserts at import that every elided entry has both spellings. ARTICLES: a CLOSED list — `al-`, `ul-`, the article assimilated to each of the fourteen sun letters in ASCII and DMG spellings (`ar-`, `az-`, `aš-`, `ash-`, `aṭ-`, …; lām's being `al-` itself), and the Egyptian `el-` with its assimilations in ASCII spellings only (`es-`, `esh-`, `ed-`, …; no `eš-` or `eṣ-`) — all never-given, where the bare `al` stays ambiguous (Al Gore): written apart with its hyphen, the word is the article and nothing else. RENDERING: the attached family joins with a space like every particle, `d' Ivoi` and `al- Rashid`. Declined: a shape rule (a vowel, consonants, a hyphen) in place of the list. It would need a new predicate and the bare-word collision check the issue named, for a population the list already covers; the corpora attest seven article spellings (`al- az- aš- ar- es- as- aṭ-`), all in it — recompute: every whitespace- or comma-bounded token of `tools/differential/corpus*.jsonl` matching `\w{1,4}-` at a name's end. Not decided here: the GLUED forms (`Paul d'Ivoi`, `Harun al-Rashid`) stay one word with no particle split, out of #700's scope by its own statement. Hyphen-minus only: U+2010 and the non-breaking hyphen are not listed. The Scottish Gaelic `a’` (`Mac a’ Ghobhainn`, in corpus_cldr.jsonl) is mid-name and not an index-form particle, and was not added. Nor are the elisions no corpus holds (`dall'`, `nell'`, `degl'`): the list is closed, so such a spelling reads as a middle name until it is listed. - RENDERING FOLLOWED THE VOCABULARY, found in review: R4's `capitalized()` split a token at its mark and asked the particle test of the stem, so `IVOI, PAUL D'` repaired to `Paul D' Ivoi` — 30 of the 32 first-listed spellings, all but `al-` and `de’`, whose stems are particles themselves. The whole token is now asked the particle arm's question first, and lowercased only where that arm would lowercase it; wherever the arm declines (an all-particle part, the given or suffix role, a recorded casing) the token goes to the per-word walk as before — a first version sent every whole-token particle down the arm's other clauses instead, which moved `smith, john, ed-` from 'ED-' to 'Ed-' and a configured `van-der` from 'Van-Der' to 'Van-der' (the fix commit's own review). The one shipped word outside #700 it reaches is `'t`, whose stem is `t`: `van 't Hoff` repairs to 'van 't Hoff', where 1.4.0 through 2.4.2 wrote 'van 'T Hoff' — R4's reading, and the right Dutch one. A casing configured for a stem (`capitalization_exceptions` `al` → `AL`) no longer reaches the marked spelling, which is now a particle in its own right: `al-` is lowercased. The snapshot cannot see a render view (decisions.md#R4); measured instead by `capitalized()` and `capitalized(force=True)` against `00dd2759`, over every name in `de6c045d`'s `tools/differential/corpus*.jsonl` as written, lowercased and uppercased (28 names move, every move lowercasing one token ending in `'` or `-`), and over every particle in eight positions under all three orders (adding `'T` → `'t`, which no corpus name held until R4's `van 't hoff` example joined corpus_rules.jsonl). The boundary `Ivoi, d'` (family `Ivoi d'`, as `Jong, de` reads) was taken out of rules.md#P6, whose statement implies given `d'`; it stays pinned as a case row. + RENDERING FOLLOWED THE VOCABULARY, found in review: R4's `capitalized()` split a token at its mark and asked the particle test of the stem, so `IVOI, PAUL D'` repaired to `Paul D' Ivoi` — 30 of the 32 first-listed spellings, all but `al-` and `de’`, whose stems are particles themselves. The whole token is now asked the particle arm's question first, and lowercased only where that arm would lowercase it; wherever the arm declines (an all-particle part, the given or suffix role, a recorded casing) the token goes to the per-word walk as before — a first version sent every whole-token particle down the arm's other clauses instead, which moved `smith, john, ed-` from 'ED-' to 'Ed-' and a configured `van-der` from 'Van-Der' to 'Van-der' (the fix commit's own review). The one shipped word outside #700 it reaches is `'t`, whose stem is `t`: `van 't Hoff` repairs to 'van 't Hoff', where 1.4.0 through 2.4.2 wrote 'van 'T Hoff' — R4's reading, and the right Dutch one. A casing configured for a stem (`capitalization_exceptions` `al` → `AL`) no longer reaches the marked spelling, which is now a particle in its own right: `al-` is lowercased. The snapshot cannot see a render view (decisions.md#R4); measured instead by `capitalized()` and `capitalized(force=True)` against `00dd2759`, over every name in `de6c045d`'s `tools/differential/corpus*.jsonl` as written, lowercased and uppercased (28 names move, every move lowercasing one token ending in `'` or `-`), and over every particle in eight positions under all three orders (adding `'T` → `'t`, which no corpus name held until R4's `van 't hoff` example joined corpus_rules.jsonl). The boundary `Ivoi, d'` was taken out of rules.md#P6: P6 does not attach there, and the reading it gets instead (family `Ivoi d'`, as `Jong, de` has read since 2.0) is the never-given vocabulary's family-comma reading, which P6's statement does not describe — a P6 example would have pinned another rule's output. It moves from given `d'` (1.4.0 through 2.4.2, the word being no particle there), and so does every new entry standing alone after a comma (`Smith, al-`); it stays pinned as a case row. Measured 2026-10-17 against the parent commit `b4a004c3`: 39 corpus names move, every one the same shape — a trailing `d'` or article leaves the middle name and joins the family (38 in corpus_gnd.jsonl, `Orval, Pierre d'` in corpus_conventions.jsonl), and no ambiguity kind changes. Recompute: `uv run python tools/differential/snapshot.py --since b4a004c3`. That is evidence about the corpus as much as the change: these are the comma-bearing names ending in one of the new forms, and GND's index forms are where they occur. ### P7 — a one-letter particle against the initial it spells diff --git a/docs/design/rules.md b/docs/design/rules.md index 46a9f780..0a804ff8 100644 --- a/docs/design/rules.md +++ b/docs/design/rules.md @@ -952,7 +952,8 @@ P6. Rationale: a particle ending the name has nothing to link A particle an index form writes apart with its elision or its hyphen still on it is a particle like any other here, where the vocabulary lists that spelling — the elided "d'" and the Arabic - article in its assimilated spellings, "al-" among them — and of + article in its plain and assimilated spellings, "al-" among them + — and of the never-given kind, since written apart the word is the article and nothing else. It joins with a space like every particle; written glued to the family ("d'Ivoi") it is one word and this diff --git a/docs/release_log.rst b/docs/release_log.rst index cf4e6a15..1848a57e 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -11,7 +11,7 @@ Release Log - **Add Chinese and Japanese credential, office and generational words as suffixes.** ``彭德威 议员`` under ``locales.ZH`` gives family ``彭``, given ``德威``, suffix ``议员``, where 2.1 through 2.4.2 gave middle ``议员``; ``王雅婷,議員`` gives suffix ``議員``, where it gave given ``議員``. Read when written as a separate word or after a comma; no peel splits them off a glued name (``陳怡君二世`` is unchanged), though the Chinese pack's surname split exposes them after a listed surname (``王二世``). :issue:`671` - **Add the Akan and Indonesian salutations owura and bapak as titles.** ``Bapak Dwi Putro`` gives title ``Bapak``, first ``Dwi``, last ``Putro``, where 1.4.0 through 2.4.2 gave first ``Bapak``, middle ``Dwi``. :issue:`662` - **Fix a title written before a family comma reading as part of the family name.** ``Dr. Smith, John`` gives title ``Dr.``, last ``Smith``, where 1.4.0 through 2.4.2 gave last ``Dr. Smith``. Only listed titles are read there, so ``Xyz. Smith, John`` keeps its family, and a title that is also a surname particle, such as ``St`` or ``Freiherr``, stays in the surname. :issue:`701` - - **Fix a surname particle such as d' or al- reading as a middle name when a listing puts it after the given name.** ``Ivoi, Paul d'`` gives last ``d' Ivoi``, where 1.4.0 through 2.4.2 gave middle ``d'``; ``Zaʿīm, Ḥusnī az-`` gives last ``az- Zaʿīm``, and the Arabic article reads the same in its other assimilated spellings. A particle written joined to the surname, as in ``Paul d'Ivoi``, is unchanged. :issue:`700` + - **Fix a surname particle such as d' or al- reading as a middle name when a listing puts it after the given name.** ``Ivoi, Paul d'`` gives last ``d' Ivoi``, where 1.4.0 through 2.4.2 gave middle ``d'``. The Arabic article reads the same in its plain and assimilated spellings (``al-``, ``az-``, ``aš-``). A particle written joined to the surname, as in ``Paul d'Ivoi``, is unchanged. :issue:`700` - **Fix case repair capitalizing the Dutch particle 't.** ``jacobus henricus van 't hoff`` repairs to ``Jacobus Henricus van 't Hoff``, where 1.4.0 through 2.4.2 gave ``van 'T Hoff``. :issue:`700` - **Add Gov as a title.** ``Gov Smith`` gives title ``Gov``, last ``Smith``, where 1.4.0 through 2.4.2 gave first ``Gov``, and ``Lt. Gov. Smith, John`` gives title ``Lt. Gov.`` with the change above. :issue:`701` diff --git a/nameparser/_render.py b/nameparser/_render.py index f4856dac..03234345 100644 --- a/nameparser/_render.py +++ b/nameparser/_render.py @@ -341,8 +341,9 @@ def _lowers_as_particle(normalized: str, word: str, role: Role, tags: frozenset[str], lex: Lexicon) -> bool: """_cap_word's particle arm: `word` (folded to `normalized`) is a particle doing a particle's work, so it is written in lowercase. - Shared with _cap_text, which asks it of a whole marked token - before splitting the token into words (#700).""" + Shared with _cap_text, which asks it of each whole token before + splitting the token into words, so a marked one ("d'") is asked as + written rather than by its stem (#700).""" return (normalized in lex.particles and not _spells_an_initial(normalized, word) and role in (Role.MIDDLE, Role.FAMILY) @@ -568,9 +569,9 @@ def cap(word: str) -> str: # mark, asking the particle test of its stem ('d', 'az', 't') # instead -- so rules.md#R4's "a particle is written in lowercase" # is asked of the whole token first. Only the lowercase is taken - # here; wherever the arm declines (an all-particle part, the given - # or suffix role, a recorded casing) the token goes to the walk - # exactly as before. + # here; wherever the arm declines (an all-particle part, a role + # other than middle or family, an initial's spelling, a recorded + # casing) the token goes to the walk exactly as before. if _lowers_as_particle(_normalize(text), text, role, tags, lex): return text.lower() if "-" not in text: diff --git a/nameparser/config/particles.py b/nameparser/config/particles.py index 4c80590c..583d0129 100644 --- a/nameparser/config/particles.py +++ b/nameparser/config/particles.py @@ -56,6 +56,7 @@ # A BARE entry, here or in PARTICLES below, means nobody has examined # it. Absence of a comment is the audit record. "'t", # Dutch contraction of 'het' ("'t Hooft"): the article itself + '’t', # the same, typographic apostrophe (#700) 'af', # Danish/Norwegian nobiliary "of" 'auf', # German "upon" ("auf der Heide") 'av', # Swedish/Norwegian "of" @@ -72,7 +73,7 @@ 'de’', # the same, typographic apostrophe: nothing folds U+2019 # to U+0027, so each elided entry is listed in both (#700) "d'", # French/Italian elided "de" ("Ivoi, Paul d'"). Written - 'd’', # apart only where an index form moves it behind the + 'd’', # apart mostly where an index form moves it behind the # given name; glued ("d'Ivoi") it stays one word (#700) "dell'", # Italian elided "dello"/"della" ("dell'Acqua") 'dell’', @@ -82,8 +83,9 @@ # attached and set behind the given name ("Rashid, Harun al-", # "Zaʿīm, Ḥusnī az-"), in its assimilated spellings: 'al-' before a # moon letter, assimilated to each of the fourteen sun letters - # (DMG's ṯ ḏ š ṣ ḍ ṭ ẓ beside the ASCII digraphs), and the Egyptian - # 'el-' and its assimilations. The trailing hyphen is the license: + # (lām's being 'al-' itself; DMG's ṯ ḏ š ṣ ḍ ṭ ẓ beside the ASCII + # digraphs), and the Egyptian 'el-' and its assimilations, in ASCII + # spellings only. The trailing hyphen is the license: # written apart, the word is the article and nothing else, so these # are never-given where the bare 'al' (Al Gore) is AMBIGUOUS. A # closed list rather than a shape rule, by decision on #700. ASCII @@ -313,6 +315,11 @@ assert not (NON_GIVEN_NAME_PARTICLES & BOUND_GIVEN_NAMES), \ "NON_GIVEN_NAME_PARTICLES must stay disjoint from BOUND_GIVEN_NAMES" assert_normalized("PARTICLES", PARTICLES) +# Nothing folds U+2019 to U+0027, so an elided entry spelled with one +# apostrophe and not the other reads only half its spellings (#700). +assert {w.replace("’", "'") for w in PARTICLES if "’" in w} \ + == {w for w in PARTICLES if "'" in w}, \ + "every elided particle needs its ASCII and its U+2019 spelling" # 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 diff --git a/tests/v2/cases.py b/tests/v2/cases.py index 536cd7bc..2ba81558 100644 --- a/tests/v2/cases.py +++ b/tests/v2/cases.py @@ -1961,11 +1961,40 @@ def _check_cjk_shape_purity(self) -> None: classification="fix(#700)", notes="AGENTS.md's comma sweep: a suffix in its own part leaves " "the given part's attachment alone. 1.4.0 middle \"d'\""), + Case("an_elided_particle_beside_a_glued_comma", + "Ivoi, Paul d',Jr.", + {"given": "Paul", "family": "d' Ivoi", "suffix": "Jr."}, + classification="fix(#700)", + notes="AGENTS.md's separator spacing: the comma glued to the " + "apostrophe still ends the particle's token. 1.4.0 " + "through 2.4.2 middle \"d'\""), + Case("an_elided_particle_after_a_particle_surname", + "van Ivoi, Paul d'", + {"given": "Paul", "family": "d' van Ivoi"}, + classification="fix(#700)", + notes="a full name with its own particle before the comma; the " + "attachment writes the trailing one in front of it. 1.4.0 " + "through 2.4.2 middle \"d'\""), + Case("an_elided_particle_after_an_empty_head", ", Paul d'", + {"given": "Paul", "family": "d'"}, + classification="fix(#700)", + notes="nothing before the comma names a family, so P6's " + "attachment has nothing to join and the particle is the " + "family by position, as ', Paul de' reads. 1.4.0 through " + "2.4.2 middle \"d'\" with no family"), + Case("an_elided_particle_in_its_typographic_spelling_t", + "Hooft, Gerard ’t", + {"given": "Gerard", "family": "’t Hooft"}, + classification="fix(#700)", + notes="the U+2019 twin of the Dutch 't, listed with the other " + "elided twins. 1.4.0 through 2.4.2 middle '’t'"), Case("an_elided_particle_alone_after_a_family_comma", "Ivoi, d'", {"family": "Ivoi d'"}, + classification="fix(#700)", notes="boundary: no given word remains to spare, so P6 does not " - "attach -- the part reads as 'Jong, de' does, family " - "'Jong de'"), + "attach; 'd'' being never-given now, the part reads as " + "'Jong, de' has since 2.0, family 'Jong de'. 1.4.0 through " + "2.4.2 gave given \"d'\", the word being no particle there"), Case("the_arabic_article_after_a_family_comma", "Rashid, Harun al-", {"given": "Harun", "family": "al- Rashid"}, classification="fix(#700)", diff --git a/tests/v2/test_render.py b/tests/v2/test_render.py index 80ffc87f..e3885eae 100644 --- a/tests/v2/test_render.py +++ b/tests/v2/test_render.py @@ -5,7 +5,8 @@ import pytest from nameparser._policy import CapsSuffixes -from nameparser import FAMILY_FIRST, HumanName, Parser, Policy, parse +from nameparser import (FAMILY_FIRST, FAMILY_FIRST_GIVEN_LAST, HumanName, + Parser, Policy, parse) from nameparser._lexicon import Lexicon from nameparser.config import Constants from nameparser._render import _apply_mask, _collapse, render @@ -1693,3 +1694,42 @@ def test_a_caps_credential_after_a_comma_keeps_its_capitals_in_every_setting( ("John Smith, PhD XYZ", "John Smith PhD XYZ")): name = parser.parse(text) assert str(name.capitalized(force=True)) == rendered, (setting, text) + + +def test_a_marked_particle_lowercases_only_where_the_particle_arm_would( +) -> None: + """rules.md#R4, #700: a particle written with its mark ("d'", + "al-", "'t") is asked the particle arm's question as a whole token, + since the per-word walk splits it at the mark and asks the stem. + Only the arm's lowercase is taken whole; wherever the arm declines, + the token repairs by the walk exactly as before. The first version + sent every whole-token particle through the rest of _cap_word, and + the full suite passed it: 'ED-' became 'Ed-' and a configured + 'van-der' became 'Van-der' (the #700 fix-commit review). Both + surfaces for the lowercase; the declines on the core.""" + lowered = { + "IVOI, PAUL D'": "Paul d' Ivoi", + "rashid, harun al-": "Harun al- Rashid", + "jacobus henricus van 't hoff": "Jacobus Henricus van 't Hoff", + } + for text, expected in lowered.items(): + assert str(parse(text).capitalized()) == expected, text + hn = HumanName(text) + hn.capitalize(force=True) + assert str(hn) == expected, text + declined = { + # an all-particle part: repaired as ordinary name words + ", PAUL D'": "Paul D'", + "HARUN AL-": "Harun Al-", + # the suffix role: the walk's acronym clause reads the stem + "smith, john, ed-": "John Smith ED-", + } + for text, expected in declined.items(): + assert str(parse(text).capitalized()) == expected, text + given_last = Parser(policy=Policy(name_order=FAMILY_FIRST_GIVEN_LAST)) + name = given_last.parse("IVOI PAUL D'") + assert name.given == "D'" # the declared given slot, not P6's + assert name.capitalized().given == "D'" + hyphened = Parser(lexicon=Lexicon.default().add(particles={"van-der"})) + assert str(hyphened.capitalized(hyphened.parse("jan van-der"))) == \ + "Jan Van-Der" From c7ee4f119c78c1da70d5d93a5528c358c9d36707 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sat, 17 Oct 2026 18:14:32 -0700 Subject: [PATCH 5/5] Ask the marked-particle check only of a token the walk could split (#700) _cap_text's whole-token check ran for every token, costing every capitalized() two frames per token ('john smith' forced: 48 -> 52). A wholly alphabetic token is one word to the walk, which asks _cap_word the same arm, so the check is gated on `not text.isalpha()`; the simplifier's sweep over 147,070 role x tag x spelling combinations found no output difference. Co-Authored-By: Claude Opus 5.5 --- nameparser/_render.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nameparser/_render.py b/nameparser/_render.py index 03234345..036d8798 100644 --- a/nameparser/_render.py +++ b/nameparser/_render.py @@ -571,8 +571,12 @@ def cap(word: str) -> str: # is asked of the whole token first. Only the lowercase is taken # here; wherever the arm declines (an all-particle part, a role # other than middle or family, an initial's spelling, a recorded - # casing) the token goes to the walk exactly as before. - if _lowers_as_particle(_normalize(text), text, role, tags, lex): + # casing) the token goes to the walk exactly as before. A wholly + # alphabetic token is one word to the walk, which asks _cap_word this + # same arm of it, so only a token the walk could split pays here. + if (not text.isalpha() + and _lowers_as_particle(_normalize(text), text, role, tags, + lex)): return text.lower() if "-" not in text: return _sub_words(cap, text)