From e17b09e8668b80939de4d99b620d06bf95f7fd23 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Fri, 30 Oct 2026 03:00:37 -0700 Subject: [PATCH 1/5] Add the ga, gd and pt-BR language packs (3.0, 3h) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit locales.GA chooses ga, turning on the Irish clan particles nic, mhic, mic and uí, written default = false in ga.toml; locales.GD chooses gd, turning on nic; locales.PT_BR chooses pt-BR (and pt), turning on sb as Sobrinho's agnomen spelling. Each pack is a choice with no fragment and no patch, so it declares no DEVIATES: its surface is the words its choice changes, which the non-interference gate derives. nic's and sb's exclusions become the readings their packs turn on. F and N, which #843 left to this pack, stay excluded, the pack included: measured on tse-donors, the letter abbreviated a surname about twice as often as it wrote the agnomen. mhic stays out of gd.toml, where it mostly links a patronymic chain a second particle would split. The default does not move: _compiled.py is unchanged, the committed snapshot passes, and tse-donors matches master's base. CLDR's ga, gd and pt renderings are recorded under their packs (no move). Co-Authored-By: Claude Opus 5.5 --- README.rst | 2 +- .../S11-suffix-word-that-is-also-a-name.md | 3 +- docs/design/decisions/vocabulary-model.md | 8 + docs/design/mechanisms.md | 2 +- .../S11-suffix-word-that-is-also-a-name.md | 6 +- docs/design/vocabulary-model-rfc.md | 4 +- docs/locales.rst | 59 +++- docs/release_log.rst | 1 + nameparser/config/_index.py | 5 + nameparser/locales/__init__.py | 3 + nameparser/locales/ga.py | 30 ++ nameparser/locales/gd.py | 33 ++ nameparser/locales/pt_br.py | 35 ++ nameparser/vocab/ga.toml | 43 ++- nameparser/vocab/gd.toml | 10 + nameparser/vocab/pt-BR.toml | 11 +- tests/v2/rules_budget.json | 2 +- tests/v2/test_locales.py | 162 ++++++++- tools/differential/cldr_agreement.py | 4 +- .../differential/cldr_agreement_locales.json | 330 ++++++++++++++++++ 20 files changed, 734 insertions(+), 19 deletions(-) create mode 100644 nameparser/locales/ga.py create mode 100644 nameparser/locales/gd.py create mode 100644 nameparser/locales/pt_br.py diff --git a/README.rst b/README.rst index 5d040ae49..af95f8e30 100644 --- a/README.rst +++ b/README.rst @@ -82,7 +82,7 @@ Quick Start Example >>> russian.parse("Сидоров Иван Петрович").family 'Сидоров' >>> locales.available() - ('cs', 'hr', 'ja', 'ne', 'ru', 'sk', 'sl', 'tr_az', 'zh') + ('cs', 'ga', 'gd', 'hr', 'ja', 'ne', 'pt_br', 'ru', 'sk', 'sl', 'tr_az', 'zh') Learn more ---------- diff --git a/docs/design/decisions/S11-suffix-word-that-is-also-a-name.md b/docs/design/decisions/S11-suffix-word-that-is-also-a-name.md index fd4f2e07c..117c17d90 100644 --- a/docs/design/decisions/S11-suffix-word-that-is-also-a-name.md +++ b/docs/design/decisions/S11-suffix-word-that-is-also-a-name.md @@ -10,7 +10,7 @@ - The floor holds before a generation word's case: a lower-case `fils` in a mixed-case name is the generation only where the name keeps a family word, so `Dumas fils` and `Pedro Junior fils` read family 'fils' — 2026-10-25 (Derek), #890, THE FLOOR. -- The abbreviations Brazilian filings write for the agnomens are agnomen spellings `same_as` their word, gated as it is: `fh`, `fo`, `filh` (filho), `net`, `nt` (neto), `sob`, `sobr` (sobrinho), each with a `family` reading, counted or declared, so that a name of two words keeps it (`Ana Fo` → family 'Fo'); `fil` and `sb` are refused on the word, and the single letters `F` and `N` are left as initials — 2026-10-27 #843. +- The abbreviations Brazilian filings write for the agnomens are agnomen spellings `same_as` their word, gated as it is: `fh`, `fo`, `filh` (filho), `net`, `nt` (neto), `sob`, `sobr` (sobrinho), each with a `family` reading, counted or declared, so that a name of two words keeps it (`Ana Fo` → family 'Fo'); `fil` is refused on the word, `sb` is read only under a Brazilian Portuguese choice, and the single letters `F` and `N` are left as initials, under that choice too — 2026-10-27 #843; 2026-10-30 #753, phase 3h. Open: none in this file. @@ -24,3 +24,4 @@ Open: none in this file. - 2026-10-25 (Derek), #890 (its accepted lower-case `fils` superseded by 2026-10-25 (Derek), #890, THE FLOOR) — A SUFFIX WORD IN FRONT OF AN AGNOMEN IS NO WORD TO SPARE. SUPERSEDES: the ACCEPTED sentence of 2026-10-25 #752 on a generation word or dotted initials in front of an agnomen. Derek approved the fix before merge, on the language-packs session's finding: the entry above accepted that a generation word or credential straight before an agnomen joined it in the suffix, leaving no family name, as a cost of 5 tse names, but that count was of capitals only, and the shape is ordinary in mixed case (`Pedro Junior Filho`, `Pedro Júnior Neto`, `Pedro Jr. Filho`, each read with no family name). One word was counted twice: the count took `Junior` as the agnomen's word to spare and the peel then took `Junior` as a suffix; and an unambiguous suffix word in front made the agnomen a credential by rules/S7's company. A generation word that is also a name now counts only the name words in front of it, skipping what the trailing run takes there (a suffix word, a numeral of two or more letters read as the generation, which the review of the fix found still counted in `Pedro VI Filho`, a member of the ambiguous class its writing makes a suffix, `Pedro MA Filho`, and a bare member any of those carries in front of it, `Pedro Junior Ma Filho` and `Pedro Junior do Filho`, the second review's), and keeps no company, so `Pedro Junior Filho` reads given 'Pedro', middle 'Junior', family 'Filho' as master did, while `Pedro Alves Junior Filho` still reads suffix 'Junior Filho'; dotted initials in front read as master did too (`PEDRO C.M. FILHO` → middle 'C.M.', family 'FILHO'), and so does a credential (`Pedro PhD Neto` → family 'Neto'). The count asks a scan that stops at the first word the walk keeps, not the walk itself: asking the walk over the words in front was tried and is exponential on a run of agnomens, quadratic when memoized, and the scaling guard (`agnomens`, `carried_agnomens` in tests/v2/test_benchmark.py) refuses both. The lower-case `fils` of a mixed-case name is settled by its case and not the count, so `Pedro Junior fils` still reads suffix 'Junior fils' (rules/S11's Accepted). - 2026-10-25 (Derek), #890, THE FLOOR — A NAME KEEPS ITS FAMILY WORD BEFORE A GENERATION WORD'S CASE IS READ. SUPERSEDES: the lower-case `fils` sentence of 2026-10-25 (Derek), #890, and the 2026-10-25 #752 entry's reading of `Dumas fils`. Derek: "apply the floor to fils too". In the gate the agreed design orders (decisions/vocabulary-model, 2026-10-23 #752), the floor, a name keeping a word to address, is the first step and the form cue the second, so case may settle a generation word against the name only where the floor already holds. A lower-case `fils` in a mixed-case name had been taken with no word to spare, as an acronym's capitals are, so `Dumas fils` read given 'Dumas' with no family name and `Pedro Junior fils` suffix 'Junior fils'. Both now read family 'fils', as master did; `Alexandre Dumas fils` and `Pedro Alves Junior fils` keep the suffix, `Marie Fils` her family name, and after a comma, where a family is already named, lower case still settles it (`Dumas, Alexandre fils` → suffix 'fils'). The same holds after a comma with no name before it, the review of the fix found: `, Dr. fils` and `(Pe), Dr. fils` had read suffix 'fils' with no family, the comma decision taking the lower case where no family is named, and read family 'fils' now, as master did; and its second review found the head's name-word count too generous for this, a joined salutation (`Mr. and Mrs., fils`) and a lone name word behind a given-name title (`Sir Alexandre, fils`) counting one where no family remains, so at the first slot after a comma a generation word's lower case adds nothing to rules/C4's count, which reads two name words before the comma first: `Dumas, fils` reads given 'fils', family 'Dumas', as master did, and `Alexandre Dumas, fils` suffix 'fils'. An acronym keeps its capitals' lean with no word to spare (rules/S6, `Jack MA`); an agnomen has no case to weigh, so no other gated word changes. Measured with `--base` against master 225f3d6f: no corpus name moves against the commit before. - 2026-10-27 #843 — THE AGNOMENS' ABBREVIATIONS ARE SPELLINGS OF THE AGNOMEN. Brazilian donors' filings (tse-donors) abbreviate the agnomen where the registry spells it, and #843's last comment counted the filed/registry pairs identical but for that last word (2026-10-25): FILHO as FH 230, FO 99, FILH 17, F 15, FIL 10; NETO as NT 122, NET 12, N 6; SOBRINHO as SOBR 19, SB 11, SOB 4. After #890 the registry's agnomen read as the suffix and the filed abbreviation as the family name, so each such pair read apart. Each abbreviation is now an `agnomen` reading `same_as` its word in `pt-BR.toml`, with a `family` reading (dated counts where `tools/name_usage.py` found bearers, `fo` US Census 238 and `net` 118; a declared one otherwise, as `filha` has), so S11's count gates it exactly as it gates the word. Measured with `tools/differential/measure.py --base` against master 6ae585f3: tse-donors moves 1,555 names (fh 801, nt 492, fo 166, sobr 51, filh 21, net 17, sob 7), and 1,481 filed/registry pairs now read alike, none apart; PSC has no move; elsewhere 6 names move fields, five of them no person's name (an organization's acronym after a particle in four JRC-Names spellings, and an FEC committee name) and one an FEC person's name in surname, given-name order ending in NET, which moves from middle to suffix as `Smith, John NET` does, a cost accepted with S11's (rules/S5 reads the given part's last word by its writing); 17 more gain a report. The words also move the comma shapes, which is what a credential reading of `nt` (13 NPPES providers) would want: `John Smith, NT` read given 'NT', family 'John Smith' on master and reads suffix 'NT' now, and `Smith, John NT` and `Smith, John NET` move from middle to suffix; `John Smith, NET` was a suffix already. Two were measured and refused on the word, with the reason as an `excluded` entry: `fil`, a Slavic and Polish surname (US Census 220), which took two UK PSC filers' surname Fil to the suffix, one a regression, against 11 tse moves; and `sb`, also the S.B. degree that 23 ClinicalTrials.gov officials write, which `capitalized(force=True)` recased to `Sb` and which would read as Sobrinho's spelling. The single letters `F` and `N` are not listed: written exactly as an initial is, nothing in the writing tells them apart, so `JOSE SILVA F` keeps family 'F'. `Fº`, the ordinal spelling, appears in no tse-donors pair (filings are in plain capitals), so it was not listed. +- 2026-10-30 #753, phase 3h — `SB` IS SOBRINHO'S SPELLING UNDER A BRAZILIAN PORTUGUESE CHOICE; `F` AND `N` STAY OUT, THE CHOICE INCLUDED. The 2026-10-27 #843 entry left `sb` refused for its collision with the S.B. degree and `F` and `N` as initials, both for a `pt-BR` pack. `sb` is now an agnomen reading `default = false` in pt-BR.toml, gated by the count as `sobrinho` is, with `cased = "SB"`; `locales.PT_BR` turns it on, and it moved 13 tse-donors names beyond what choosing `pt` moves, the registry spelling SOBRINHO in each. `F` and `N`, turned on the same way, read the letter as the agnomen where the registry abbreviated a surname with it about twice as often as where it wrote the agnomen (`F` 104 to 48, `N` 59 to 18), so each is an `excluded` agnomen with those counts, and `JOSE CARLOS SILVA F` keeps family 'F' under the pack. The measurement and recipe are in decisions/vocabulary-model's 2026-10-30 phase 3h entry. diff --git a/docs/design/decisions/vocabulary-model.md b/docs/design/decisions/vocabulary-model.md index 252671f96..46d50fd6b 100644 --- a/docs/design/decisions/vocabulary-model.md +++ b/docs/design/decisions/vocabulary-model.md @@ -10,6 +10,7 @@ The vocabulary was a set of base sets plus marker subsets (`particles` and `part - The default loads every language and keeps working for names of any language. Choosing a language is to narrow it in phase 3 (#753): a pack is to settle only the clashes where its language has a vocabulary reading, a strict pack to drop other languages' name-level readings too, and a pack's preferred reading to be able to outrank one a shape rule derives — 2026-10-15/16. - `e`, `y` and `i` stay in the default; choosing a language settles their one-case fork for that language's own reading, `may_be_initial` not holding on a chosen file's conjunction, which no shipped pack does yet — 2026-10-15/16; 2026-10-29 #753, phase 3b. - A patronymic field is to come in 3.0, as a view that names its words without emptying `family`, and the particle kinds already draw the `clan`/`patronymic` line for it: a patronymic is followed by a parent's name, a clan prefix by a family name the lineage shares — 2026-10-15/16; 2026-10-19 #749; 2026-10-28 #754 (Derek); landed as rules/R9, 2026-10-29 #754, sub-phase 4c. +- A shipped pack that is only a choice of languages declares no `DEVIATES`, its surface the words its choice changes; `ga` turns on `nic`, `mhic`, `mic` and `uí` as clan particles, `gd` `nic`, and `pt-BR` `sb` as Sobrinho's spelling, while the single letters `f` and `n` are excluded, the pack included, the letter abbreviating a surname more often than the agnomen in the filings that write it — 2026-10-30 #753, phase 3h. - A language pack is a choice of languages on `Locale.languages` selecting which readings compile, a tag choosing its own file and its ancestors', and a reading index, loaded on its first lookup, carries the languages to run time; strict waits for 3i, and third-party language files are deferred — 2026-10-27 #753 (Derek); 2026-10-29 #753, phase 3a (Derek); 2026-10-29 #753, phase 3b. - A choice is matched without regard to case, a tag whose file does not exist selects its ancestors' (`pt-PT` selects `pt`), `mul` and `und` are no language to choose, and a reading turned on also prefers, so `de`'s generation `i` drops `ca`'s and `pl`'s conjunction under a `de` choice; a choice is one call's: one chained through `base=` applies on top of the base's rather than unioning with it, and a choice drops the caller's own base entries it removes — 2026-10-29 #753, phase 3b; 2026-10-29 #753, phase 3b, chained choices (Derek). - A reading right in its own language and wrong only as a default, because another language written in the same script collides with it, is written `default = false` in its file, never excluded, so that after the packs an exclusion means wrong everywhere: cs `st` since phase 2h, and cs `ml`, hr, sk and sl `ml` and `st` and fr `père` since 3c, the rest in their packs' PRs — 2026-10-27 #753 (Derek); 2026-10-29 #753, phase 3c. @@ -351,3 +352,10 @@ Open: #913 (the title words phase 2j left, read as titles though borne as names) - **Measured** 2026-10-30 on `40675146` plus this change, Python 3.11.17. Committed corpora, `snapshot.py --languages TAG`, entries moved (held only by `corpus_cldr.jsonl` among them): `cs` 17 (6), `sk` 15 (4), `sl` 15 (4), `hr` 15 (4), `pl` 11 (2). Fifteen of each are `st` and `ml` moves, 3c's fourteen and rules/S11's new `[cs]` example `Jiří Dienstbier st.`, which this change adds to the rules corpus; cs and pl add `Pan Tomasz Nowak` and `Pan Nowak Tomasz Jan` (under `FAMILY_FIRST`), both now reading title `Pan`; pl's other 9 are its conjunction `i`, which choosing pl already moved. The CLDR locale view, newly recorded for the four packs in `cldr_agreement_locales.json`: whole renderings agreeing cs 14/24, sk 15/24, sl 16/24, hr 18/24, default and pack alike; the pack moves two sl renderings and no other, each `ml.` from family to suffix (`g. Janez Peter Novak ml., univ. dipl. inž.`, family `Novak`), both still disagreeing elsewhere. Local corpora against the base's default readings (`snapshot.py --local SOURCE --base --languages TAG`), counted by the first changed word each moved name holds: `ctgov-officials` cs 178 (st 69, mudr 44, pan 17, drsc 15 and 2 `Dr. Sc.`, phdr 12, rndr 7, pharmdr 6, ml 3, and one each of paeddr, mddr, mvdr), sk 161 (the same less pan), sl and hr 72 (st 69, ml 3); `orcid-researchers` cs 245 (st 124, pan 46, phdr 16, ml 14, mudr 11, judr 8, drsc 6, mga 5, paeddr 5, rndr 4, roz 3, and one each of thdr, pharmdr, bca), sk 191, sl and hr 138 (st 124, ml 14). Read by kind: the titles and `drsc` are fixes wherever they move a field (`Denisa Wilkes, MUDr` stops reversing, `Christian Salmon, DrSc` keeps its given name, `Prof. MUDr.` reads one title run); every `pan` move is a Chinese name read with a title (`PAN DAI` → title `PAN`), no Czech salutation among them; `roz` fixes one (`Lenka Šenkárová (roz. Veverková)` → maiden `Veverková`) and costs two given names `Roz` (`ELLA ROZ PERRY` → maiden `PERRY`); `st` is 3c's (Saint names, and `PhD St.`, a PhD student, read as the generation). The fielded scores, `local_agreement.read` over the records of the four countries: ORCID's 56,808 records from CZ, SK, SI and HR agree 52,239 under the default and 52,240 under cs and sk (`dr.sc. Sara Soldo`, HR, fixed), unchanged under sl and hr; PSC's 3,578 Czech, Slovak, Slovenian and Croatian filers agree 3,505 under every pack, cs and sk fixing `Mudr Jozef Stec` and moving `Mr Judr Branislav Fridich`, whose filer typed `Judr` in the forename box. The same run over `psc-individuals`: cs 416 (st 395, pan 16, mudr 2, and one each of judr, mvdr and roz, `Miss Naila Roz Roz` → maiden `Roz`), sk 399, sl 395 (all st; hr not run, its choice changing the same two words the same way); `tse-donors` cs 9 (st 5, roz 2, pan 2, `PAN SHISHEN` → title `PAN`); `ukec-individuals` cs 9 (st 8, and `Victor MGA Killearn`, only its forced case repair moving, `MGA` → `MgA`). Recompute: the commands above, and `uv run python -c "from nameparser import _reading_index as ri; print(sorted(ri.changed_words(ri.selected(['cs']))))"` for each tag's words. - **Found, not changed:** a gated generation behind a particle run after a family comma stays in the given part unreported, `Novák, Freiherr von Berg st.` reading given `von Berg st.` under these packs as the default reads `Dumas, Freiherr von Berg fils` (rules/S11); the default reading of the same `st.` is stranger, family `st. Novák`. Pinned in the 3f table. And with one word before a family comma, `Novák, st.` reads title `st.` by its period shape (rules/H2) under all four packs, reporting suffix-or-name with the generation as the alternative though the reading taken is a title, as the default reads `Dumas, fils.`; the ungated `Novák, ml.` reads suffix `ml.`. `Novák st., Jan` keeps family `Novák st.`, as the default keeps `Dumas fils, Alexandre`: the gated word before a family comma is a name word. Both pinned in the 3f table. - **Open:** `pan`, kept because the work order names it: the corpora show its cost (81 Chinese names across ctgov, ORCID, PSC and TSE read with the title) and no gain, the shape that excluded `bc`, and as a title on its own it also takes a lone given name after a family comma (`Smith, Pan` reads title `Pan` with no given name, a bare `Pan` title `Pan`), so whether it stays in the Czech pack is a question for the reviewer. Whether the uncontested titles (`mudr`, `judr`, `phdr`, `rndr` and the rest, no name reading and no clash found) belong in the default rather than a pack is a default change, measured on ctgov, outside phase 3's rule that nothing moves the default. +- **2026-10-30 #753, phase 3h — THE `ga`, `gd` AND `pt-BR` PACKS: THE IRISH CLAN FORMS AND `sb` TURN ON; `F` AND `N` STAY OUT, MEASURED; THE DEFAULT DOES NOT MOVE.** Carries out 3h of item 11 of the 2026-10-27 #753 entry, and for `nic` and `sb` the rest of item 9. + - **What each pack turns on.** `locales.GA` chooses `ga`, `locales.GD` `gd` and `locales.PT_BR` `pt-BR` (code `pt_br`, its choice selecting `pt` too, item 6 of the phase 3b entry), each with an empty lexicon fragment and no policy patch. ga.toml: `nic` (daughter of), its exclusion converted, with a `given` reading (US SSA 77, Wikidata 173) and a `family` one (US Census 108), so it is a possibly-given particle and a leading `Nic` stays given, reported (rules/P1); `mhic` and `uí`, the married forms of `mac` and `ó`, `may_be_given = false`; `mic`, the genitive of `mac`, with `given` (US SSA 26) and `family` readings (ORCID's family box: Romania 4, Ireland none). Each clan reading is `default = false` and `same_as` its head where it has one. gd.toml: `nic` alone. pt-BR.toml: `sb`, its exclusion converted to an `agnomen` reading `default = false`, `same_as = "sobrinho"`, with a declared `family` reading as its siblings have, and `cased = "SB"`, so the S.B. degree's capitals survive `capitalized(force=True)` under the pack, the recasing that was half of its exclusion's reason. Counts as of 2026-10-30 from `tools/name_usage.py nic mic mhic uí --boxes`. + - **`mhic` stays out of gd.toml.** Written in Scottish Gaelic it mostly links a patronymic chain, `Iain mac Dhòmhnaill mhic Sheumais`, which the default reads family `mac Dhòmhnaill mhic Sheumais` only because `mhic` is unlisted; listed, it starts a second particle run and the chain splits at it, middle `mac Dhòmhnaill`, family `mhic Sheumais`, as the default already splits `Iain mac Dhòmhnaill mac Sheumais`. The Irish married form (`Máire Mhic Gabhann`) is the word's modern written use, so ga.toml keeps it and accepts the chain's split (`Seán mac Seáin mhic Thomáis`, pinned nowhere and moving no corpus name). + - **`F` and `N` are excluded, the pack included.** #843 left them to this pack. Turned on, both letters measured on tse-donors (2026-10-30, `snapshot.py --local tse-donors --languages pt-BR --base` against `--languages pt`, each moved name scored by its registry spelling): of the names they moved beyond what `pt` moves, `F` was the agnomen in 48 and a letter abbreviating a surname in 104 (`ANA MARIA F`, registry `ANA MARIA FILGUEIRAS`), `N` in 18 and 59 (`EDINEI VITAL N`, registry `EDINEI VITAL DO NASCIMENTO`), last or before another generation word alike (`ADINALDO VILHENA F JUNIOR`, registry `FERREIRA JUNIOR`). The filings are the evidence the letters were proposed from, and they abbreviate the other words too, so no writing tells the two apart even inside the language: each is an `excluded` agnomen in pt-BR.toml with these counts. `sb` moved 13 names' roles beyond `pt`'s, the registry spelling SOBRINHO in every one (the filed/registry pairs reading alike, 11 under `pt` and the default, rose to 22), and one more name's `capitalized()` view alone, its mask writing a mid-name `SB` (`VIVIANE G A DA SB FERRUGINI`) where the default writes `Sb`. + - **The default does not move.** The build reports `_compiled.py` current; tests/v2/test_snapshot.py passes with no re-record; `snapshot.py --local tse-donors --base` against master `40675146` matches the base. + - **What each choice moves,** measured 2026-10-30 on `40675146` plus this change, Python 3.11. Committed corpora (`snapshot.py --languages TAG`): `ga` and `gd` 4 entries each, every one `mc` (`John Smith MC` family `MC`, where the default reads the Military Cross), the prefer of the phase 3b entry's turning-on bullet on the `mc` both files already held; `pt-BR` the same 38 as `pt` (1 held only by `corpus_cldr.jsonl`), `sb` moving none. CLDR's locale view (`cldr_agreement.py`, `ga`, `gd` and `pt` mapped to the packs, CLDR's `pt` being Brazilian): `ga` 14/24, `gd` 15/24, `pt` 17/24 renderings agreeing with and without the pack, recorded. Fielded registers, scored over every record holding a word the choice changes (`local_agreement.py`'s `read` under each parser): PSC, `ga` 4 fixed (`Clodagh Nic Lochlainn`, `Orlaith Mhic Mhathuna`, and `Damien Patrick Mc` by the `mc` prefer) and 1 broken (`Nicholas Nic Rawlings Limited Rawlings`, a company filed as a person), `gd` 3 and the same 1; ORCID, `ga` 24 fixed and none broken over every country (23 in Ireland and Great Britain, `Pauline Uí Dhuibhir`, `Éimí Mhic an Ridire`), `gd` 20 and none (19 in Ireland and Great Britain). `pt-BR`'s moves there are all `pt`'s: PSC 12 broken, each a trailing `Sr` read as the title Senhor (`Gary Holloway Sr` family `Sr`), ORCID 21 fixed (the one-case `e`) and 11 broken. + - **Open for 3g:** a `pt` choice prefers `pt.toml`'s title `sr` (Senhor) over English's generation, so a trailing `Sr` reads as a title or the family name under `pt` and `pt-BR` (PSC's 12 above); whether the generation survives a Portuguese choice is 3g's, which owns `pt`'s titles. The recipe for the fielded figures is the scratch scorer this entry describes: `local_agreement.read` per record under `Parser()` and `parser_for(pack)`, over the records whose name holds a word of `changed_words(selected(pack.languages))`. diff --git a/docs/design/mechanisms.md b/docs/design/mechanisms.md index 9f2ca5f61..7a5d26a8e 100644 --- a/docs/design/mechanisms.md +++ b/docs/design/mechanisms.md @@ -430,7 +430,7 @@ A second axis cuts across the jobs: whether the names are real or invented, labe **How it works.** Agreement is not correctness, so this is a second measure beside a SCORE (mechanisms.md#NAME-DATA-JOBS), never in place of one. Its trap is a comparison that agreed for the wrong reason. #752's first version compared the family's LAST WORD as written, and gating the Brazilian agnomens counted 1,043 tse pairs "now apart" (2026-10-24): on master both spellings had agreed on `FILHO` as the family, and once the agnomen read as a suffix the comparison reached the real surname, which the two spellings write differently. Comparing whole fields after folding cut the agnomen gate's apart count to 304, and reading those by shape showed what was left: a word differing between the spellings, an initial, a length difference, and 47 differing only by accent or case, which point at the one real defect: the spike asked a case cue the design does not. Across scripts the same measure found what no single-script corpus could: taking `Guru` out of the titles pushed 44 Nepali pairs apart because its Devanagari twin stayed a title. -**Reach for it when.** A change moves a word between fields in a population that holds the same names spelled twice; and when a vocabulary word has a twin in another script, which a pair source shows and a single-script snapshot cannot. +**Reach for it when.** A change moves a word between fields in a population that holds the same names spelled twice; and when a vocabulary word has a twin in another script, which a pair source shows and a single-script snapshot cannot. And when a filed spelling ABBREVIATES its twin's words, so that most moved pairs read apart before and after: alike-or-apart then counts only the moves that land exactly, and a move from one wrong reading to another is silent. #753's phase 3h counted `pt-BR`'s `F`, `N` and `SB` at 34 pairs fixed and none broken by that measure, while 208 moved pairs read apart both ways; asked what the registry wrote at the moved word (`F` beside `FILHO`, or `F` beside a surname), 163 of all 242 moves were a letter abbreviating a surname, and `F` and `N` stayed out (2026-10-30). Score each move by the twin's word at the place the change acts, not only by whole-name agreement. ### Field notes — the traps themselves diff --git a/docs/design/rules/S11-suffix-word-that-is-also-a-name.md b/docs/design/rules/S11-suffix-word-that-is-also-a-name.md index 2e18038f3..bb40e3d82 100644 --- a/docs/design/rules/S11-suffix-word-that-is-also-a-name.md +++ b/docs/design/rules/S11-suffix-word-that-is-also-a-name.md @@ -4,7 +4,7 @@ **Then:** it reads as the generation only where the name has words to spare, as a bare ambiguous acronym does (S4), and at the slots that report, either reading carries the ambiguity flag. -- **The writing is read by S6**, and for a generation word such as French `fils` it speaks at the other end of the case: lower case in a name of mixed case is the generation, Title case leans to the name, capitals say nothing. The floor holds first: lower case settles it only where the name keeps a family word, so two name words keep the family, and at the first slot after a comma the count of name words before it decides alone (C4). An agnomen, the Portuguese `Filho`, `Neto`, `Sobrinho` and their feminines, is written in the case of the name around it, so its writing never speaks and the count decides alone; so is each abbreviation of one that Brazilian filings write (`FH`, `Fo`, `Nt`, `Sobr`), a spelling of the word it shortens that takes the word's name reading with it, borne or not. +- **The writing is read by S6**, and for a generation word such as French `fils` it speaks at the other end of the case: lower case in a name of mixed case is the generation, Title case leans to the name, capitals say nothing. The floor holds first: lower case settles it only where the name keeps a family word, so two name words keep the family, and at the first slot after a comma the count of name words before it decides alone (C4). An agnomen, the Portuguese `Filho`, `Neto`, `Sobrinho` and their feminines, is written in the case of the name around it, so its writing never speaks and the count decides alone; so is each abbreviation of one that Brazilian filings write (`FH`, `Fo`, `Nt`, `Sobr`), a spelling of the word it shortens that takes the word's name reading with it, borne or not. Under a Brazilian Portuguese choice so is `SB`, which by default reads as the name word the S.B. degree is. - **Only name words in front of it count**: a word there that the trailing run takes, a suffix word, a roman numeral read as the generation (S10), a member of the ambiguous class its writing makes a suffix (S6) or one such a word carries in front of it (S7), is no word to spare, and a suffix word there is no company either, as it is for an acronym (S7). - **After a comma** the word is read as a member of the ambiguous class is: by the count of the name words before the comma at the first slot after it (C4), and by its writing alone where it ends the given part after a family comma (S5). @@ -16,6 +16,10 @@ "Pedro Alves Neto" → ambiguities=("suffix-or-name",) "PEDRO DA SILVA FILHO" → suffix="FILHO" "JOSE DA SILVA FH" → suffix="FH" +"JOSE CARLOS SILVA SB" [pt_br] → suffix="SB" +"JOSE CARLOS SILVA SB" → family="SB" · boundary +"Ana SB" [pt_br] → family="SB" · boundary +"JOSE CARLOS SILVA F" [pt_br] → family="F" · boundary "Pedro Alves Sobr." → suffix="Sobr." "Pedro Alves Neto, PhD" → suffix="Neto, PhD" "Pedro Alves Neto Júnior" → suffix="Neto Júnior" diff --git a/docs/design/vocabulary-model-rfc.md b/docs/design/vocabulary-model-rfc.md index 00260702f..357772fbd 100644 --- a/docs/design/vocabulary-model-rfc.md +++ b/docs/design/vocabulary-model-rfc.md @@ -164,7 +164,7 @@ Each sub-phase is one PR, measured on its own base: the committed snapshot read ## Phase 3: packs that prefer or restrict -Status: **agreed** (proposed 2026-10-27, #753; Derek accepted every item on 2026-10-28, six and then the other six); **3a landed 2026-10-29**, the reading index, the compiler moved into the package and `default = false` on name readings (decisions/vocabulary-model's 2026-10-29 entry, which records what 3a decided and measured, and the index's lazy load, which Derek chose on 2026-10-29 over loading it with `Lexicon.default()`; "Where the provenance lives at runtime" below describes it). **3b landed 2026-10-29**, the choice: `Locale.languages`, its tag selection, turning on, prefer and the shape clash of `may_be_initial`, the derived deviation surface, `snapshot.py --languages`, and mechanisms.md#LOCALE-PACKS-PURE-DATA amended (decisions/vocabulary-model's 2026-10-29 phase 3b entry, which records what it decided, measured and left open). **3c landed 2026-10-29**, the eight pack-shaped exclusions (cs `ml`, hr, sk and sl `st` and `ml`, fr `père`) written as `generation` readings `default = false`, the default's compile unchanged (decisions/vocabulary-model's 2026-10-29 phase 3c entry, which records what each choice now moves). **3d landed 2026-10-30**, the first language pack, `locales.NE`, a choice of `ne` turning on `given` readings of `श्री`, `guru` and `गुरु` written `default = false` in `ne.toml`, its deviations derived rather than declared, and `local_corpora.py alike --languages` for the pair measure (decisions/vocabulary-model's 2026-10-30 phase 3d entry, which records what it moves and the one defect left open). **3f landed 2026-10-30**, the Czech, Slovak, Slovenian and Croatian packs, `locales.CS`, `SK`, `SL` and `HR`, one `Locale` per language as the API section below has it, each, like `NE`, a choice of its one tag and nothing else: 3c's generations, cs and pl `pan` and cs `roz` converted from exclusions, and the Czech and Slovak academic titles and post-nominals, all readings written `default = false`, the default's compile unchanged (decisions/vocabulary-model's 2026-10-30 phase 3f entry, which records what each pack moves and settles 3c's three open questions). The record is decisions/vocabulary-model's 2026-10-27 #753 entry; this section describes its items. One design inside the work order is still open: 3g's connective (below), which accepting the work order did not decide. Phase 3 is 3.0 work (re-planned 2026-10-28): 2.8 is the last 2.x feature release, and 3.0 ships phases 3 and 4 with the facade's removal. Packs stay opt-in, so a caller who chooses no language parses as the default does, and nothing in this phase moves the default's parses. One standing text is amended with it, in 3b's PR, which builds the selection: mechanisms.md#LOCALE-PACKS-PURE-DATA's contract that a pack is add-only and declares its deviations in its docstring (a pack adds, and selects among the default's readings by language, its deviations derived for a vocabulary pack). decisions/3-0-reevaluations' line that removing base vocabulary through a pack is part of 3.0's pack design is carried out rather than moved. +Status: **agreed** (proposed 2026-10-27, #753; Derek accepted every item on 2026-10-28, six and then the other six); **3a landed 2026-10-29**, the reading index, the compiler moved into the package and `default = false` on name readings (decisions/vocabulary-model's 2026-10-29 entry, which records what 3a decided and measured, and the index's lazy load, which Derek chose on 2026-10-29 over loading it with `Lexicon.default()`; "Where the provenance lives at runtime" below describes it). **3b landed 2026-10-29**, the choice: `Locale.languages`, its tag selection, turning on, prefer and the shape clash of `may_be_initial`, the derived deviation surface, `snapshot.py --languages`, and mechanisms.md#LOCALE-PACKS-PURE-DATA amended (decisions/vocabulary-model's 2026-10-29 phase 3b entry, which records what it decided, measured and left open). **3c landed 2026-10-29**, the eight pack-shaped exclusions (cs `ml`, hr, sk and sl `st` and `ml`, fr `père`) written as `generation` readings `default = false`, the default's compile unchanged (decisions/vocabulary-model's 2026-10-29 phase 3c entry, which records what each choice now moves). **3d landed 2026-10-30**, the first language pack, `locales.NE`, a choice of `ne` turning on `given` readings of `श्री`, `guru` and `गुरु` written `default = false` in `ne.toml`, its deviations derived rather than declared, and `local_corpora.py alike --languages` for the pair measure (decisions/vocabulary-model's 2026-10-30 phase 3d entry, which records what it moves and the one defect left open). **3f landed 2026-10-30**, the Czech, Slovak, Slovenian and Croatian packs, `locales.CS`, `SK`, `SL` and `HR`, one `Locale` per language as the API section below has it, each, like `NE`, a choice of its one tag and nothing else: 3c's generations, cs and pl `pan` and cs `roz` converted from exclusions, and the Czech and Slovak academic titles and post-nominals, all readings written `default = false`, the default's compile unchanged (decisions/vocabulary-model's 2026-10-30 phase 3f entry, which records what each pack moves and settles 3c's three open questions). **3h landed 2026-10-30**, the packs `ga` (`nic`, `mhic`, `mic`, `uí`), `gd` (`nic`) and `pt-BR` (`sb`), each a choice with no fragment and no patch, its deviations derived; `F` and `N` stay excluded, the pack included, measured wrong more often than right in the filings that write them (decisions/vocabulary-model's 2026-10-30 phase 3h entry). The record is decisions/vocabulary-model's 2026-10-27 #753 entry; this section describes its items. One design inside the work order is still open: 3g's connective (below), which accepting the work order did not decide. Phase 3 is 3.0 work (re-planned 2026-10-28): 2.8 is the last 2.x feature release, and 3.0 ships phases 3 and 4 with the facade's removal. Packs stay opt-in, so a caller who chooses no language parses as the default does, and nothing in this phase moves the default's parses. One standing text is amended with it, in 3b's PR, which builds the selection: mechanisms.md#LOCALE-PACKS-PURE-DATA's contract that a pack is add-only and declares its deviations in its docstring (a pack adds, and selects among the default's readings by language, its deviations derived for a vocabulary pack). decisions/3-0-reevaluations' line that removing base vocabulary through a pack is part of 3.0's pack design is carried out rather than moved. **Phase 4's shape questions came first.** The output model decides what a pack can express: a patronymic field, a second surname, credentials apart from generation suffixes (#326), and what `middle` means (#704). A pack reading of a kind with no field to land in is a reading nothing can surface, so those questions were to be settled before or with phase 3's mechanism, by the same shape-before-value rule, and the output model was agreed on 2026-10-28 (decisions/vocabulary-model's 2026-10-28 #754 entry): the seven roles stay, and `generation`, `credentials`, `patronymic` and `surname2` are views read from `Token.reading` or rules/O6's tag. Of the work order below, 3a (the reading index and the shared compiler) and 3c (the pack-shaped exclusions converted) are independent of the output model: the index carries whatever kinds the files hold, and 3c moves data between two encodings that compile alike. 3b's selection semantics are independent too, and its API's dependency, what `Locale` and `Policy` look like once the facade is gone, is answered (below). The packs depend on the output model in part, each on the phase 4 sub-phase that builds its view: `cs`'s generations and `pt-BR`'s agnomens on 4b's `generation` (3f and 3h), `es` and `pt` on 4d's `surname2` and 4g's compound given name (3g); `ne`'s readings do not. @@ -263,7 +263,7 @@ Shape first, then the packs, each one PR measured on its own base, as phase 2's | 3e | `hi` and `bn`, the Latin forms (`smt`, `shrimati`, `sushri`, `shriyut`, `shri`, `adhyapak`), the ones whose collisions prefer cannot settle left for strict (#345) | 3b | | 3f | `cs`, `sk`, `sl`, `hr`: the generations of 3c, `pan`, `roz` and the academic abbreviations #848 left | 3b, 3c, 4b | | 3g | `es` and `pt`: the occupational titles and `Don`/`Doña`/`Dona`, with a connective that counts only inside a title run, its own rule; `pt` also settles the one-case `e` | 3b, 4d, 4g | -| 3h | `ga` and `gd` (`nic`, `mhic`, `mic`, `uí`), `pt-BR` (`F`, `N`, `sb`); `nl`'s prepositions wait with strict | 3b, 4b | +| 3h | `ga` and `gd` (`nic`, `mhic`, `mic`, `uí`), `pt-BR` (`F`, `N`, `sb`); `nl`'s prepositions wait with strict. Landed with `mhic` and `mic` in `ga` only and `pt-BR` as `sb` alone (the 2026-10-30 phase 3h entry) | 3b, 4b | | 3i | the languages of each name reading's bearers, an attribute compiling to nothing, filled word by word from per-country shares and whole-box counts (above); strict and its candidates after it | 3a (neutral) | **Open:** 3g's connective needs a design of its own, not decided with the work order, because the pair that separates it is a count: `Profesora de Historia Ana López` should read title `Profesora de Historia`, while `Doctora de la Vega`, the same title, preposition and following word, has no name behind it and must keep family `de la Vega`. With `profesora` and `ministro` added as titles and no rule, the first reads family `de Historia Ana López` (the rules/P1 fold), and `Ministro da Fazenda Carlos Souza` reads given `da`. The rule must reach only readings a pack turns on, so the default's parses stay where they are. diff --git a/docs/locales.rst b/docs/locales.rst index 832965547..b0694a156 100644 --- a/docs/locales.rst +++ b/docs/locales.rst @@ -321,7 +321,7 @@ look one up dynamically by its lowercase code with .. doctest:: >>> locales.available() - ('cs', 'hr', 'ja', 'ne', 'ru', 'sk', 'sl', 'tr_az', 'zh') + ('cs', 'ga', 'gd', 'hr', 'ja', 'ne', 'pt_br', 'ru', 'sk', 'sl', 'tr_az', 'zh') >>> locales.get("ru") is locales.RU True @@ -344,6 +344,16 @@ Shipped packs ``st.``, and in Czech and Slovak the academic titles written before the name and the degrees written after it (see :ref:`west-south-slavic-packs`). + * - ``ga`` + - Irish — chooses ``ga``, which turns on the clan particles the + default leaves off: ``Nic`` (daughter of), the married forms + ``Mhic`` and ``Uí``, and ``Mic`` (``Máire Uí Bhriain`` → family + ``Uí Bhriain``). ``Nic`` and ``Mic`` are also given names, so + one opening the name stays the given name, reported. See + `Packs that choose a language`_. + * - ``gd`` + - Scottish Gaelic — chooses ``gd``, which turns on ``Nic`` + (``Màiri Nic Leòid`` → family ``Nic Leòid``). * - ``ja`` - Japanese segmentation — activates division for unspaced Japanese names, which needs a segmenter to act: install @@ -359,6 +369,13 @@ Shipped packs given ``श्री`` in the default order, where the default gives title ``श्री``). Each stays a title inside a title phrase (``His Holiness Guru Prasad``). + * - ``pt_br`` + - Brazilian Portuguese — chooses ``pt-BR``, and with it ``pt``, + which turns on ``SB``, the agnomen Sobrinho as Brazilian filings + abbreviate it, read as a suffix where the name has words to + spare (``JOSE CARLOS SILVA SB`` → suffix ``SB``), and settles + Portuguese's clashes its way. + See `Packs that choose a language`_. * - ``ru`` - East Slavic patronymic order — detects the formal family/given/patronymic shape (three name words, the last ending @@ -385,7 +402,7 @@ Shipped packs Japanese name read with it can split after its first character (``安藤一郎`` → family ``安``), so use ``ja`` for Japanese data. -``cs``, ``hr``, ``ne``, ``sk`` and ``sl`` are each a choice of languages +``cs``, ``ga``, ``gd``, ``hr``, ``ne``, ``pt_br``, ``sk`` and ``sl`` are each a choice of languages and nothing else: the readings each turns on are the default vocabulary's own, written in its language's file (``ne.toml`` for Nepali) and off until a caller chooses the language. ``ja``, ``ru`` and @@ -455,6 +472,40 @@ a middle name with the ending, so it follows the order: under and no patronymic. A marker and the word before it are read under every order. +Packs that choose a language +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some words are right in one language and wrong as a default, because +another language written in the same script reads them differently. +Irish writes a woman's surname with ``Nic`` (daughter of) or, married, +``Uí`` (of the descendant of) before it, but ``Nic`` is also the short +form of Nicholas; Brazilian filings abbreviate the agnomen *Sobrinho* +(nephew) to ``SB``, which is also the S.B. degree. The default reads +none of them, and ``ga``, ``gd`` and ``pt_br`` turn them on: + +.. doctest:: + + >>> parse("Máire Uí Bhriain").family + 'Bhriain' + >>> parser_for(locales.GA).parse("Máire Uí Bhriain").family + 'Uí Bhriain' + >>> parse("JOSE CARLOS SILVA SB").family + 'SB' + >>> brazil = parser_for(locales.PT_BR).parse("JOSE CARLOS SILVA SB") + >>> brazil.family, brazil.suffix + ('SILVA', 'SB') + +Each word is then read as its language reads the full word: ``SB`` is a +suffix only where the name keeps a family name without it (``Ana SB`` +keeps family ``SB``), and ``Nic`` opening a name stays the given name +(``Nic Cage``). Each reports its doubt in +``ambiguities``. A choice also settles every clash its language has a +reading for, as `Choosing languages`_ shows: under ``ga`` and ``gd``, +``Mc`` is the clan particle and not the Military Cross +(``John Smith MC`` → family ``MC``), and ``pt_br`` reads as ``pt`` +does (``John Smith DO`` → family ``DO``, ``J. Smith Sr.`` → title +``Sr.``, Senhor). + .. _segmenter-contract: Segmenters @@ -582,7 +633,7 @@ by ``tests/v2/test_locales.py``: docstring explains how the pack-contract test enforces this. A pack's choice of languages is never declared: its deviations are derived from the words the choice changes, so a pack that is nothing - but a choice (``ne``, ``cs``) declares no ``DEVIATES`` — the + but a choice (``ne``, ``cs``, ``ga``) declares no ``DEVIATES`` — the contract test refuses one, and refuses a choice that changes no word — and one with a fragment or a patch beside its choice declares only what those change. @@ -600,7 +651,7 @@ by ``tests/v2/test_locales.py``: one per shape of the vocabulary it ships: single surname, compound surname, and any spelling variant it means to cover. - **By a choice of languages** (``ne``, ``cs``, ``sk``, ``sl``, - ``hr``): a name for each word the choice changes, in each script it + ``hr``, ``ga``, ``gd``, ``pt_br``): a name for each word the choice changes, in each script it is written in and each position where the default reads it otherwise — ``test_choice_rotators_cover_every_changed_word`` fails until every word is held. diff --git a/docs/release_log.rst b/docs/release_log.rst index 83da3c0ec..11c0ddc41 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -8,6 +8,7 @@ Release Log - **Add Locale.languages, a choice of languages for the default vocabulary.** ``parser_for(Locale(code="pt", lexicon=Lexicon.empty(), languages=frozenset({"pt"}))).parse("ANA SILVA E SOUZA")`` gives family ``SILVA E SOUZA``, where the default reads family ``SOUZA``, the ``E`` an initial: a chosen language's reading of a word it has a vocabulary reading of wins, and the other languages' vocabulary readings of that word leave, while its readings as a name and the readings shared by every language stay, so under ``pt`` ``John Smith DO`` reads family ``DO``. A generation word another language collides with is read only under its own language: ``Jiří Dienstbier ml.`` gives suffix ``ml.`` under ``cs``, and the Czech, Slovak, Slovenian and Croatian ``ml.`` and ``st.`` and the French ``père`` read this way. A pack choosing no language parses as before. See :doc:`locales`. :issue:`753` - **Add locales.NE, a Nepali pack, the first that is a choice of languages.** ``parser_for(locales.NE).parse("Guru Prasad Sharma")`` gives given ``Guru``, middle ``Prasad``, where the default parser gives title ``Guru``, given ``Prasad``; ``श्री`` and ``गुरु`` read the same way, each a name word placed by position (the family name under a family-first order) and still a title inside a title phrase (``His Holiness Guru Prasad``). It reads Hindi and Marathi names too, where ``श्री`` is usually the salutation, so use it for Nepali data. :issue:`753` + - **Add the Irish, Scottish Gaelic and Brazilian Portuguese packs, locales.GA, locales.GD and locales.PT_BR.** ``parser_for(locales.GA).parse("Máire Uí Bhriain")`` gives family ``Uí Bhriain``, where the default, as in 1.4.0 through 2.8.0, gives middle ``Uí``. ``GA`` reads ``Nic``, ``Mhic``, ``Mic`` and ``Uí`` as clan particles and ``GD`` ``Nic``, a leading ``Nic`` staying the given name; ``PT_BR`` reads ``SB`` as the agnomen Sobrinho, while ``F`` and ``N`` stay the name they spell. Each is a choice of languages, which settles its language's clashes its way: ``John Smith MC`` gives family ``MC`` under ``GA`` and ``GD``, and ``John Smith DO`` family ``DO`` under ``PT_BR``. See :doc:`locales`. :issue:`753` - **Add ParsedName.generation and ParsedName.credentials, the suffix split by kind.** ``parse("Smith, John, Jr., MD, Esq.")`` gives generation ``Jr.`` and credentials ``MD, Esq.``, where 1.4.0 through 2.8.0 gave only suffix ``Jr., MD, Esq.``, which is unchanged. They are views like ``family_base``: render keys, not fields of ``as_dict()``. An honorific after a name (``Esq.``, ``さん``) is a credential, and a suffix written by ``replace()``, or in a name unpickled from 2.x, is all credentials. ``Token.reading`` reads ``Jr.`` and ``Sr.`` as a generation, and ``Ambiguity.alternative`` of a lone ``Jr`` names a generation where 2.7.0 and 2.8.0 named a post-nominal. :issue:`326`, :issue:`754` - **Add ParsedName.patronymic, the words naming the bearer's parent.** ``parse("Mahathir bin Mohamad")`` gives patronymic ``bin Mohamad`` beside family ``bin Mohamad``, which 1.4.0 through 2.8.0 gave alone and which is unchanged; under ``locales.RU``, ``Ivan Ivanovich Ivanov`` gives patronymic ``Ivanovich`` and keeps it as the middle name, and under ``locales.TR_AZ`` a marker such as ``oglu`` reads with the name before it. A view like ``family_base``, not a field of ``as_dict()``, read only from a patronymic particle (``bin``, ``binti``, ``binte``, ``ibn``, ``bint``, ``bar``, ``bat`` and their Arabic and Hebrew forms) or a rule a locale turns on, so ``Anwar Ibrahim`` has none, and a name written by ``replace()``, or unpickled from 2.x, has none either. ``Ambiguity.alternative`` of a leading ``bin`` read as a given name names a patronymic where 2.7.0 and 2.8.0 named a particle. :issue:`754` - **Add ParsedName.surname2, the second of two family names.** ``Parser(policy=Policy(two_family_names=True)).parse("Gabriel García Márquez")`` gives surname2 ``Márquez`` beside family ``García Márquez``, which 2.8.0 gave alone and which is unchanged. A view like ``family_base``, not a field of ``as_dict()``, filled only where ``two_family_names`` reads the family as two, so the default, a family comma (``Lloyd Webber, Andrew``) and a hyphenated family have none, and so does a name unpickled from 2.x or one whose family ``replace()`` rewrote. ``Reading.second_surname`` marks its words; it is not compared. :issue:`754` diff --git a/nameparser/config/_index.py b/nameparser/config/_index.py index f4d06a15b..b4a4b3026 100644 --- a/nameparser/config/_index.py +++ b/nameparser/config/_index.py @@ -1061,7 +1061,9 @@ ('mga', (('cs', 'academic', (('cased', 'MgA'), ('default', False))),)), ('mgr', (('cs', 'academic', (('default', False),)), ('en', 'religious', ()), ('fr', 'religious', ()), ('sk', 'academic', (('default', False),)))), ('mgysgt', (('en', 'military', ()),)), + ('mhic', (('ga', 'clan', (('default', False),)),)), ('mhsc', (('en', 'credential', (('cased', 'MHSc'),)),)), + ('mic', (('ga', 'clan', (('default', False),)), ('ga', 'given', ()), ('ga', 'family', ()))), ('micp', (('en', 'credential', ()),)), ('mieee', (('en', 'credential', ()),)), ('military', (('en', 'modifier', ()),)), @@ -1158,6 +1160,7 @@ ('neto', (('pt', 'agnomen', ()), ('pt', 'given', ()), ('pt', 'family', ()))), ('netto', (('pt-BR', 'agnomen', ()), ('pt-BR', 'family', ()))), ('neuroscientist', (('en', 'occupation', ()),)), + ('nic', (('ga', 'clan', (('default', False),)), ('ga', 'given', ()), ('ga', 'family', ()), ('gd', 'clan', (('default', False),)))), ('nmd', (('en', 'credential', ()),)), ('novelist', (('en', 'occupation', ()),)), ('np', (('en', 'credential', ()),)), @@ -1374,6 +1377,7 @@ ('saoshyant', (('en', 'religious', ()),)), ('sasm', (('en', 'credential', ()),)), ('satirist', (('en', 'occupation', ()),)), + ('sb', (('pt-BR', 'agnomen', (('cased', 'SB'), ('default', False))), ('pt-BR', 'family', ()))), ('sccp', (('en', 'credential', ()),)), ('scd', (('en', 'credential', (('cased', 'ScD'),)),)), ('scholar', (('en', 'occupation', ()),)), @@ -1534,6 +1538,7 @@ ('usnr', (('en', 'credential', ()),)), ('uxc', (('en', 'credential', ()),)), ('uxmc', (('en', 'credential', ()),)), + ('uí', (('ga', 'clan', (('cased', 'Uí'), ('default', False))),)), ('v', (('mul', 'generation', ()),)), ('vadm', (('en', 'military', ()),)), ('van', (('nl', 'preposition', ()), ('nl', 'given', ()))), diff --git a/nameparser/locales/__init__.py b/nameparser/locales/__init__.py index 4647a0adc..ae625be52 100644 --- a/nameparser/locales/__init__.py +++ b/nameparser/locales/__init__.py @@ -24,9 +24,12 @@ #: lowercase module names; attribute constants are uppercase. _REGISTRY = { "CS": ("nameparser.locales.cs", "CS"), + "GA": ("nameparser.locales.ga", "GA"), + "GD": ("nameparser.locales.gd", "GD"), "HR": ("nameparser.locales.hr", "HR"), "JA": ("nameparser.locales.ja", "JA"), "NE": ("nameparser.locales.ne", "NE"), + "PT_BR": ("nameparser.locales.pt_br", "PT_BR"), "RU": ("nameparser.locales.ru", "RU"), "SK": ("nameparser.locales.sk", "SK"), "SL": ("nameparser.locales.sl", "SL"), diff --git a/nameparser/locales/ga.py b/nameparser/locales/ga.py new file mode 100644 index 000000000..1b3da12ec --- /dev/null +++ b/nameparser/locales/ga.py @@ -0,0 +1,30 @@ +"""The Irish locale pack: vocabulary-only -- it chooses the language +``ga``, which turns on the clan readings nameparser/vocab/ga.toml writes +``default = false``: ``nic`` (daughter of, beside ``mac``), the married +forms ``mhic`` and ``uí`` (of ``mac`` and ``ó``), and the genitive +``mic`` (decisions/vocabulary-model, the 2026-10-30 phase 3h entry). +Each is then a particle that joins the surname behind it: +``Máire Uí Bhriain`` gives family ``Uí Bhriain`` where the default +gives middle ``Uí``. ``nic`` and ``mic`` are given names too, so a +leading one stays the given name, reported (rules/P1). + +Data sources: the counts on each reading in ga.toml (tools/name_usage.py, +ORCID's family box by country among them). The pack carries no lexicon +fragment and no policy patch. + +Declared deviations (mechanisms.md#LOCALE-PACKS-PURE-DATA): a +vocabulary pack's deviations are derived, not declared -- the words +whose compiled readings its choice changes +(``_reading_index.changed_words``), which the non-interference gate in +tests/v2/test_locales.py reads -- so this module defines no DEVIATES. +""" +from __future__ import annotations + +from nameparser._lexicon import Lexicon +from nameparser._locale import Locale + +GA = Locale( + code="ga", + lexicon=Lexicon.empty(), + languages=frozenset({"ga"}), +) diff --git a/nameparser/locales/gd.py b/nameparser/locales/gd.py new file mode 100644 index 000000000..cdd8a7194 --- /dev/null +++ b/nameparser/locales/gd.py @@ -0,0 +1,33 @@ +"""The Scottish Gaelic locale pack: vocabulary-only -- it chooses the +language ``gd``, which turns on the clan reading +nameparser/vocab/gd.toml writes ``default = false``: ``nic`` (daughter +of, beside ``mac``), written spaced (decisions/vocabulary-model, the +2026-10-30 phase 3h entry). It is then a particle that joins the +surname behind it: ``Màiri Nic Leòid`` gives family ``Nic Leòid`` where +the default gives middle ``Nic``. The genitive ``mhic``, which the +Irish pack turns on, is left out: in Scottish Gaelic it mostly links a +patronymic chain, which a second particle would split (gd.toml's note). +``Nic`` is a given name too (its reading is in ga.toml, and holds in +every choice), so a leading Nic stays the given name, reported +(rules/P1). + +Data sources: the counts on each reading in ga.toml and gd.toml +(tools/name_usage.py). The pack carries no lexicon fragment and no +policy patch. + +Declared deviations (mechanisms.md#LOCALE-PACKS-PURE-DATA): a +vocabulary pack's deviations are derived, not declared -- the words +whose compiled readings its choice changes +(``_reading_index.changed_words``), which the non-interference gate in +tests/v2/test_locales.py reads -- so this module defines no DEVIATES. +""" +from __future__ import annotations + +from nameparser._lexicon import Lexicon +from nameparser._locale import Locale + +GD = Locale( + code="gd", + lexicon=Lexicon.empty(), + languages=frozenset({"gd"}), +) diff --git a/nameparser/locales/pt_br.py b/nameparser/locales/pt_br.py new file mode 100644 index 000000000..b91f1c7d0 --- /dev/null +++ b/nameparser/locales/pt_br.py @@ -0,0 +1,35 @@ +"""The Brazilian Portuguese locale pack: vocabulary-only -- it chooses +the language ``pt-BR``, which selects nameparser/vocab/pt-BR.toml and +its ancestor pt.toml, and turns on the agnomen reading pt-BR.toml +writes ``default = false``: ``SB``, Sobrinho abbreviated as Brazilian +filings write it, which outside Brazil is the S.B. degree +(decisions/vocabulary-model, the 2026-10-30 phase 3h entry). It is then +gated by the count as ``sobrinho`` is (rules/S11): ``JOSE CARLOS SILVA +SB`` gives suffix ``SB`` where the default gives family ``SB``. The +single letters ``F`` and ``N`` the same filings write for Filho and +Neto stay out, the pack included: in those filings the letter more +often abbreviates a surname (pt-BR.toml's exclusions). +Choosing ``pt-BR`` also chooses ``pt``, whose readings it prefers where +another language's clash (``do`` a particle, not the DO credential). + +Data sources: tse-donors, Brazilian candidates' receipts as filed beside +the registry's spelling (tools/differential/local_tse.py); the counts +are on the readings in pt-BR.toml. The pack carries no lexicon fragment +and no policy patch. + +Declared deviations (mechanisms.md#LOCALE-PACKS-PURE-DATA): a +vocabulary pack's deviations are derived, not declared -- the words +whose compiled readings its choice changes +(``_reading_index.changed_words``), which the non-interference gate in +tests/v2/test_locales.py reads -- so this module defines no DEVIATES. +""" +from __future__ import annotations + +from nameparser._lexicon import Lexicon +from nameparser._locale import Locale + +PT_BR = Locale( + code="pt_br", + lexicon=Lexicon.empty(), + languages=frozenset({"pt-BR"}), +) diff --git a/nameparser/vocab/ga.toml b/nameparser/vocab/ga.toml index 5964b3ce1..1092c22cf 100644 --- a/nameparser/vocab/ga.toml +++ b/nameparser/vocab/ga.toml @@ -20,11 +20,37 @@ clan = { expands = "son of" } text = "mc" clan = { may_be_given = false, issue = 360, expands = "son of, Mac contracted", same_as = "mac" } +[[word]] +text = "mhic" +[word.clan] +default = false +may_be_given = false +issue = 753 +expands = "of the son of, the genitive of 'mac' after a woman's 'bean' or name: \"Máire Mhic Gabhann\", the married form of Mac Gabhann" +same_as = "mac" +note = "Off by default with the rest of the Irish married and female forms: a particle the default has never read, turned on by an Irish pack (gd.toml says why Scottish Gaelic leaves it out). No source counts it as a given name (2026-10-30: ORCID 2 family boxes, both Ireland; PSC 1 filed name; none as a given name)." + +[[word]] +text = "mic" +given = { us_ssa = 26, wikidata = 10, as_of = 2026-10-30, note = "male births only; Wikidata is a lower bound; a short form of Michael" } +family = { wikidata = 1, as_of = 2026-10-30, note = "ORCID's family box holds it 13 times, 4 of them in Romania (2026-10-30), the Romanian surname Mic, none in Ireland" } +[word.clan] +default = false +issue = 753 +expands = "of the son of, the unlenited genitive of 'mac'" +same_as = "mac" +note = "Off by default: Mic is also a given name, a short form of Michael (its given reading), and a Romanian surname. An Irish pack turns it on, its given reading keeping a leading Mic a given name there, reported (rules/P1)." + [[word]] text = "nic" -[word.excluded.clan] -reason = "the Irish \"daughter of\" beside 'ní', but also an abbreviation of Nicolas and Nicole and a given name of its own (Nic Cage): a leading Nic would be marked ambiguous at best, and its home is an opt-in Irish pack" -issue = 604 +given = { us_ssa = 77, wikidata = 173, as_of = 2026-10-30, note = "male births only; Wikidata is a lower bound; nam_dict band 4 in the Netherlands, a short form of Nicholas and Nicolaas (Nic Cage)" } +family = { us_census_2010 = 108, as_of = 2026-10-30, note = "ORCID's family box holds it 73 times, 22 of them in Ireland (2026-10-30), the Irish surname written spaced" } +[word.clan] +default = false +issue = [604, 753] +expands = "daughter of, the female form beside 'mac': \"Máire Nic Gabhann\"" +gender = "f" +note = "Off by default: Nic is also a given name, the short form of Nicholas (its given reading), and the default serves every language written in Latin script, where an English Nic is far likelier than an Irish particle (#604). An Irish or Scottish Gaelic pack turns it on, and its given reading keeps a leading Nic a given name there, reported (rules/P1)." [[word]] text = "ní" @@ -61,3 +87,14 @@ expands = "descendant of, the older spelling: \"Seán Ua Buachalla\"" same_as = "ó" cased = "Ua" note = "No given-name use found, but searched less thoroughly than 'ó' and 'ní', and C-i defaults to AMBIGUOUS under uncertainty -- so the leading \"Ua Buachalla\" keeps the given reading and reports it, while mid-name the chain is the same either way (#604). The mask is the word's own title case, as on 'ní': the dotted 'u.a.' repairs to 'U.A.'" + +[[word]] +text = "uí" +[word.clan] +default = false +may_be_given = false +issue = 753 +expands = "of the descendant of, the genitive of 'ó' after a woman's 'bean' or name: \"Máire Uí Bhriain\", the married form of Ó Briain" +same_as = "ó" +cased = "Uí" +note = "Off by default with 'mhic': a particle the default has never read, turned on by an Irish pack. No source counts it as a given name (2026-10-30: ORCID 15 family boxes, 6 in Ireland; none as a given name). Unaccented 'ui' is left out: written so, it is also a pair of initials. The mask is the word's own title case, as on 'ní'." diff --git a/nameparser/vocab/gd.toml b/nameparser/vocab/gd.toml index 4590e5a59..444fe58ac 100644 --- a/nameparser/vocab/gd.toml +++ b/nameparser/vocab/gd.toml @@ -7,3 +7,13 @@ clan = { expands = "son of" } [[word]] text = "mc" clan = { may_be_given = false, issue = 360, expands = "son of, Mac contracted", same_as = "mac" } + +[[word]] +text = "nic" +[word.clan] +default = false +issue = [604, 753] +expands = "daughter of, the female form beside 'mac', written spaced: \"Màiri Nic Leòid\" (glued, \"NicLeòid\" is one word and needs no particle)" +gender = "f" +note = "Off by default, as in ga.toml, which holds the word's given and family readings: a Scottish Gaelic pack turns it on, and the given reading keeps a leading Nic a given name, reported (rules/P1). The genitive 'mhic' is left out of this file though ga.toml turns it on: written in Scottish Gaelic it mostly links a patronymic chain (\"Iain mac Dhòmhnaill mhic Sheumais\"), which a second listed particle splits at its last link, as the default already splits 'Iain mac Dhòmhnaill mac Sheumais' (2026-10-30)." + diff --git a/nameparser/vocab/pt-BR.toml b/nameparser/vocab/pt-BR.toml index 2db0adadc..aeb72d250 100644 --- a/nameparser/vocab/pt-BR.toml +++ b/nameparser/vocab/pt-BR.toml @@ -1,5 +1,9 @@ schema = 1 +[[word]] +text = "f" +excluded = { agnomen = { reason = "Filho written short in 15 tse-donors filed/registry pairs (2026-10-25), but written exactly as an initial is, and in those filings mostly one: turned on by a Brazilian Portuguese choice it moved 152 tse-donors names (measured with 'n' on too) that the choice of Portuguese alone does not, and in 104 of them the registry spells a surname the letter abbreviates (ANA MARIA F, registry ANA MARIA FILGUEIRAS) where in 48 it spells FILHO (2026-10-30). No writing tells the two apart, so no pack reads it", issue = [843, 753], decided = 2026-10-30 } } + [[word]] text = "fil" excluded = { agnomen = { reason = "Filho written short in 10 tse-donors filed/registry pairs (2026-10-25), but a Slavic and Polish surname (US Census 220, Wikidata 6 with Poland and Ukraine) that a name of three words would lose to the suffix: the measurement against master moved 11 tse-donors names toward the registry and two UK PSC filers' surnames Fil away from the box they typed it in, one a regression, and a JRC-Names surname behind an initial", issue = [843, 752], decided = 2026-10-27 } } @@ -19,6 +23,10 @@ text = "fo" agnomen = { same_as = "filho", gender = "m", issue = [843, 752], expands = "the son, abbreviated", note = "tse-donors (2026-10-25) has 99 filed/registry pairs writing FO where the registry writes FILHO; NPPES has no provider writing it as a credential" } family = { us_census_2010 = 238, wikidata = 6, as_of = 2026-10-27, note = "Italian (Dario Fo) and Asian surname bearers; a name of two words keeps it as the family name, and a third word before it reads it as the agnomen, reported (rules/S11's Accepted clause)" } +[[word]] +text = "n" +excluded = { agnomen = { reason = "Neto written short in 6 tse-donors filed/registry pairs (2026-10-25), but written exactly as an initial is, and in those filings mostly one: turned on by a Brazilian Portuguese choice it moved 77 tse-donors names (measured with 'f' on too) that the choice of Portuguese alone does not, and in 59 of them the registry spells a surname the letter abbreviates (EDINEI VITAL N, registry EDINEI VITAL DO NASCIMENTO) where in 18 it spells NETO (2026-10-30). No writing tells the two apart, so no pack reads it", issue = [843, 753], decided = 2026-10-30 } } + [[word]] text = "net" agnomen = { same_as = "neto", gender = "m", issue = [843, 752], expands = "the grandson, abbreviated", note = "tse-donors (2026-10-25) has 12 filed/registry pairs writing NET where the registry writes NETO; NPPES has no provider writing it as a credential" } @@ -36,7 +44,8 @@ family = { note = "no source counts it as a name; declared as neto's is, so that [[word]] text = "sb" -excluded = { agnomen = { reason = "Sobrinho written short in 11 tse-donors filed/registry pairs (2026-10-25), but also S.B., the Bachelor of Science, written SB by 23 ClinicalTrials.gov officials (9 after a comma) and 2 NPPES providers (2026-10-27): listed as an agnomen it took the degree as Sobrinho's spelling and capitalized(force=True) recased it to Sb. A credential reading is the question to settle first", issue = [843, 752], decided = 2026-10-27 } } +agnomen = { default = false, same_as = "sobrinho", gender = "m", issue = [843, 752, 753], expands = "the nephew, abbreviated", cased = "SB", note = "Off by default: tse-donors (2026-10-25) has 11 filed/registry pairs writing SB where the registry writes SOBRINHO, but SB is also S.B., the Bachelor of Science, which 23 ClinicalTrials.gov officials (9 after a comma) and 2 NPPES providers write (2026-10-27), and listed by default it read the degree as Sobrinho's spelling. A Brazilian Portuguese pack turns it on: it then moved 13 tse-donors names that the choice of Portuguese alone does not, and the registry spells SOBRINHO in every one (2026-10-30). The mask keeps the degree's capitals, which capitalized(force=True) recased to Sb when the word was listed by default" } +family = { note = "no source counts it as a name; declared as sobrinho's is, so that two words ending in it keep it as the family name" } [[word]] text = "sob" diff --git a/tests/v2/rules_budget.json b/tests/v2/rules_budget.json index 0fbfdc57e..d32bc9761 100644 --- a/tests/v2/rules_budget.json +++ b/tests/v2/rules_budget.json @@ -70,7 +70,7 @@ "S8": {"words": 571, "then_words": 406, "because_sentences": 1, "except": 1, "precedence": 2, "accepted": 1, "accepted_words": 63}, "S9": {"words": 463, "then_words": 262, "because_sentences": 2, "except": 0, "precedence": 3, "accepted": 0, "accepted_words": 0}, "S10": {"words": 665, "then_words": 246, "because_sentences": 3, "except": 4, "precedence": 3, "accepted": 3, "accepted_words": 163}, - "S11": {"words": 511, "then_words": 298, "because_sentences": 3, "except": 1, "precedence": 1, "accepted": 2, "accepted_words": 107}, + "S11": {"words": 531, "then_words": 318, "because_sentences": 3, "except": 1, "precedence": 1, "accepted": 2, "accepted_words": 107}, "S12": {"words": 674, "then_words": 372, "because_sentences": 2, "except": 3, "precedence": 4, "accepted": 2, "accepted_words": 93}, "S13": {"words": 431, "then_words": 180, "because_sentences": 3, "except": 2, "precedence": 1, "accepted": 4, "accepted_words": 138}, "T1": {"words": 93, "then_words": 18, "because_sentences": 1, "except": 1, "precedence": 0, "accepted": 3, "accepted_words": 83}, diff --git a/tests/v2/test_locales.py b/tests/v2/test_locales.py index 4d84cd1d0..3e9fe3eeb 100644 --- a/tests/v2/test_locales.py +++ b/tests/v2/test_locales.py @@ -113,9 +113,10 @@ def test_locales_module_attribute_access() -> None: def test_locales_get_and_available() -> None: assert locales.get("ru") is locales.RU assert set(locales.available()) == { - "cs", "hr", "ja", "ne", "ru", "sk", "sl", "tr_az", "zh"} - with pytest.raises(KeyError, - match="cs, hr, ja, ne, ru, sk, sl, tr_az, zh"): + "cs", "ga", "gd", "hr", "ja", "ne", "pt_br", "ru", "sk", "sl", "tr_az", "zh"} + with pytest.raises( + KeyError, + match="cs, ga, gd, hr, ja, ne, pt_br, ru, sk, sl, tr_az, zh"): locales.get("xx") # the segmenter FACTORY is not a locale: it is an ordinary function # on the package, so it must not appear among the codes @@ -1072,6 +1073,28 @@ def test_choice_rotators_cover_every_changed_word(code: str) -> None: assert surface and missing == [] +# The vocabulary packs' rotators: a name in each shape a reading the pack +# turns on changes, read one way by default and another under the pack +# (the pinned rows below say how). ga: each clan particle mid-name; +# gd: `nic`, its one word; pt_br: `sb` behind name words, bare and +# dotted, in each case. +_ROTATORS["ga"] = [ + "Máire Nic Gabhann", # nic + "Máire Mhic Gabhann", # mhic + "Máire Uí Bhriain", # uí + "Pádraig Mic Cárthaigh", # mic +] +_ROTATORS["gd"] = [ + "Màiri Nic Leòid", # nic + "Gabhann, Màiri Nic", # nic ending the given part +] +_ROTATORS["pt_br"] = [ + "JOSE CARLOS SILVA SB", # sb + "Jose Carlos Silva Sb.", # sb, dotted + "JOSE SILVA SB", # sb, one word to spare +] + + def _marker_regexes(module: ModuleType) -> list[re.Pattern]: return [v for v in vars(module).values() if isinstance(v, re.Pattern)] @@ -1590,6 +1613,139 @@ def test_the_west_and_south_slavic_packs_repair_their_titles_case() -> None: assert str(parse("mudr. jan novák").capitalized()) == "Mudr. Jan Novák" +# -- 3h: the Irish, Scottish Gaelic and Brazilian Portuguese packs -- +# +# Each row is a name in a shape the sweeps reach (comma shapes, one +# case, dotted and bare, a leading word, a family-comma head), read by +# the default and then under the pack, as (given, middle, family, +# suffix, ambiguity kinds). The default's reading is the refusal the +# pack lifts, so a row that moved by default fails here as well as in +# the snapshot (decisions/vocabulary-model's 2026-10-30 phase 3h entry). +_GA_NIC = ("Máire", "", "Nic Gabhann", "", ()) +_3H_ROWS = [ + # the clan particles mid-name: middle by default, the surname's + # first word under the pack, as `ní` and `ó` read in both + ("GA", "Máire Nic Gabhann", ("Máire", "Nic", "Gabhann", "", ()), _GA_NIC), + ("GA", "Máire Mhic Gabhann", + ("Máire", "Mhic", "Gabhann", "", ()), ("Máire", "", "Mhic Gabhann", "", ())), + ("GA", "Máire Uí Bhriain", + ("Máire", "Uí", "Bhriain", "", ()), ("Máire", "", "Uí Bhriain", "", ())), + ("GA", "MÁIRE UÍ BHRIAIN", + ("MÁIRE", "UÍ", "BHRIAIN", "", ()), ("MÁIRE", "", "UÍ BHRIAIN", "", ())), + ("GA", "Pádraig Mic Cárthaigh", + ("Pádraig", "Mic", "Cárthaigh", "", ()), + ("Pádraig", "", "Mic Cárthaigh", "", ())), + ("GD", "Màiri Nic Leòid", + ("Màiri", "Nic", "Leòid", "", ()), ("Màiri", "", "Nic Leòid", "", ())), + # a never-given particle opening the name starts the family (rules/P1) + ("GA", "Uí Bhriain", ("Uí", "", "Bhriain", "", ()), ("", "", "Uí Bhriain", "", ())), + # Nic and Mic are given names too: a leading one stays given, reported + ("GA", "Nic Cage", + ("Nic", "", "Cage", "", ()), ("Nic", "", "Cage", "", ("PARTICLE_OR_GIVEN",))), + ("GD", "Nic Cage", + ("Nic", "", "Cage", "", ()), ("Nic", "", "Cage", "", ("PARTICLE_OR_GIVEN",))), + ("GA", "Mic Jagger", + ("Mic", "", "Jagger", "", ()), ("Mic", "", "Jagger", "", ("PARTICLE_OR_GIVEN",))), + # the comma shapes: a full surname before the comma is unchanged; + # one ending the given part joins the surname as `Ní` does by default + ("GA", "Nic Gabhann, Máire", _GA_NIC, _GA_NIC), + ("GA", "Gabhann, Máire Nic", + ("Máire", "Nic", "Gabhann", "", ()), + ("Máire", "", "Nic Gabhann", "", ("PARTICLE_OR_GIVEN",))), + ("GA", "Gabhann, Máire Ní", _GA_NIC[:2] + ("Ní Gabhann", "", ()), + _GA_NIC[:2] + ("Ní Gabhann", "", ())), + ("GA", ", Máire Uí Bhriain", + ("Máire", "Uí Bhriain", "", "", ()), ("Máire", "Uí Bhriain", "", "", ())), + # a surname that is the word alone keeps it + ("GA", "Ioana Mic", ("Ioana", "", "Mic", "", ()), ("Ioana", "", "Mic", "", ())), + # SB: the family name by default, gated by the count under the + # pack as sobrinho is (rules/S11) + ("PT_BR", "JOSE CARLOS SILVA SB", + ("JOSE", "CARLOS SILVA", "SB", "", ()), + ("JOSE", "CARLOS", "SILVA", "SB", ("SUFFIX_OR_NAME",))), + ("PT_BR", "José Silva Sb.", + ("José", "Silva", "Sb.", "", ()), + ("José", "", "Silva", "Sb.", ("SUFFIX_OR_NAME",))), + ("PT_BR", "SILVA, JOSE CARLOS SB", + ("JOSE", "CARLOS SB", "SILVA", "", ()), + ("JOSE", "CARLOS", "SILVA", "SB", ("SUFFIX_OR_NAME",))), + ("PT_BR", ", JOSE SILVA SB", + ("JOSE", "SILVA SB", "", "", ()), ("JOSE", "SILVA", "", "SB", ("SUFFIX_OR_NAME",))), + # with no words to spare it keeps the family, reported + ("PT_BR", "Ana SB", ("Ana", "", "SB", "", ()), ("Ana", "", "SB", "", ("SUFFIX_OR_NAME",))), + # an agnomen before a family comma stays in the surname, as FILHO does + ("PT_BR", "SILVA SB, JOSE CARLOS", + ("JOSE", "CARLOS", "SILVA SB", "", ()), ("JOSE", "CARLOS", "SILVA SB", "", ())), + # the dotted S.B. after a comma is paired initials either way (rules/C5) + ("PT_BR", "John Smith, S.B.", + ("S.B.", "", "John Smith", "", ("SUFFIX_OR_NAME",)), + ("S.B.", "", "John Smith", "", ("SUFFIX_OR_NAME",))), + # F and N, written as an initial is, are excluded, the pack included + # (pt-BR.toml): the family name they spell, by default and under it + ("PT_BR", "JOSE CARLOS SILVA F", + ("JOSE", "CARLOS SILVA", "F", "", ()), ("JOSE", "CARLOS SILVA", "F", "", ())), + ("PT_BR", "Pedro Alves N.", + ("Pedro", "Alves", "N.", "", ()), ("Pedro", "Alves", "N.", "", ())), + # the letter as an initial: before the surname, or leading, unchanged + ("PT_BR", "JOHN F KENNEDY", + ("JOHN", "F", "KENNEDY", "", ()), ("JOHN", "F", "KENNEDY", "", ())), + ("PT_BR", "F. Silva", ("F.", "", "Silva", "", ()), ("F.", "", "Silva", "", ())), + # a family comma with a title-particle head: the given part's walk + # keeps it a name word under the pack too + ("PT_BR", "Smith, Freiherr von Berg SB", + ("von Berg SB", "", "Smith", "", ()), ("von Berg SB", "", "Smith", "", ())), +] + + +@pytest.mark.parametrize("code, name, default, packed", _3H_ROWS, + ids=[f"{r[0]}:{r[1]}" for r in _3H_ROWS]) +def test_the_3h_packs_lift_the_defaults_refusal( + code: str, name: str, default: tuple[object, ...], + packed: tuple[object, ...]) -> None: + assert (_fields(parse(name)), + _fields(parser_for(locales.get(code)).parse(name))) == ( + default, packed) + + +@pytest.mark.parametrize("order", [FAMILY_FIRST, FAMILY_FIRST_GIVEN_LAST]) +@pytest.mark.parametrize("code, name, default, packed", [ + # a never-given particle opening the name takes one name unit under a + # family-first order (rules/P1), where the default reads the + # particle alone as the family name + ("GA", "Uí Bhriain Máire", ("Uí", ""), ("Uí Bhriain", "")), + # a possibly-given one is left to position, by default and under + # the pack alike + ("GD", "Nic Leòid Màiri", ("Nic", ""), ("Nic", "")), + # the agnomen ends the name under either order (family, suffix) + ("PT_BR", "SILVA JOSE CARLOS SB", ("SILVA", ""), ("SILVA", "SB")), +]) +def test_the_3h_packs_under_a_family_first_order( + order: object, code: str, name: str, default: tuple[str, str], + packed: tuple[str, str]) -> None: + policy = Policy(name_order=order) # type: ignore[arg-type] + by_default = Parser(policy=policy).parse(name) + by_pack = parser_for(locales.get(code), base=Parser(policy=policy)).parse(name) + assert ((by_default.family, by_default.suffix), + (by_pack.family, by_pack.suffix)) == (default, packed) + + +def test_the_pt_br_pack_selects_pt_too() -> None: + # pt-BR selects its ancestor's file (decisions/vocabulary-model's 3b + # entry, item 6): Portuguese `e` is a connective in a one-case name + packed = parser_for(locales.PT_BR) + assert parse("ANA SILVA E SOUZA").family == "SOUZA" + assert packed.parse("ANA SILVA E SOUZA").family == "SILVA E SOUZA" + + +def test_the_sb_mask_keeps_the_degrees_capitals_under_pt_br() -> None: + # SB listed as an agnomen was recased to Sb by force (pt-BR.toml's + # note); the reading's mask keeps it SB, and only under the pack + packed = parser_for(locales.PT_BR) + name = packed.parse("jose carlos silva sb") + assert str(packed.capitalized(name, force=True)) == "Jose Carlos Silva SB" + assert str(parse("jane doe sb").capitalized(force=True)) == "Jane Doe Sb" + + # -- #269: non-Latin default vocabulary (Cyrillic, Greek, Arabic, # Hebrew, Devanagari, Bengali) -- # diff --git a/tools/differential/cldr_agreement.py b/tools/differential/cldr_agreement.py index 8ec89b0ca..e7f9f8ab1 100644 --- a/tools/differential/cldr_agreement.py +++ b/tools/differential/cldr_agreement.py @@ -142,7 +142,9 @@ #: name to NO_CLDR; test_cldr_agreement.py fails on a pack in neither. LOCALE_FILES = {"zh": "ZH", "zh_Hant": "ZH", "yue": "ZH", "yue_Hans": "ZH", "ja": "JA", "ne": "NE", "ru": "RU", "tr": "TR_AZ", "az": "TR_AZ", - "cs": "CS", "sk": "SK", "sl": "SL", "hr": "HR"} + "cs": "CS", "sk": "SK", "sl": "SL", "hr": "HR", + # CLDR's pt is Brazilian Portuguese (pt_PT is its own file) + "ga": "GA", "gd": "GD", "pt": "PT_BR"} #: Packs with no CLDR file to measure them on. NO_CLDR: frozenset[str] = frozenset() #: The locale view of the native-script Wikidata source: stratum label -> diff --git a/tools/differential/cldr_agreement_locales.json b/tools/differential/cldr_agreement_locales.json index 95aba3a34..cc6ca3fb5 100644 --- a/tools/differential/cldr_agreement_locales.json +++ b/tools/differential/cldr_agreement_locales.json @@ -259,6 +259,231 @@ }, "text": "Jiří" }, + "ga/foreignFull/givenFirst": { + "surname2": { + "expected": "González Domingo", + "got": "" + }, + "text": "Prof. Dr. Ada Cornelia César Martín von Brühl González Domingo M.D. Ph.D." + }, + "ga/foreignFull/sorting": { + "surname2": { + "expected": "González Domingo", + "got": "" + }, + "text": "von Brühl González Domingo, Prof. Dr. Ada Cornelia César Martín M.D. Ph.D." + }, + "ga/foreignFull/surnameFirst": { + "family": { + "expected": "von Brühl", + "got": "von" + }, + "family_base": { + "expected": "Brühl", + "got": "von" + }, + "family_particles": { + "expected": "von", + "got": "" + }, + "given_names": { + "expected": "Ada Cornelia César Martín", + "got": "Brühl Ada Cornelia César Martín" + }, + "text": "von Brühl Ada Cornelia César Martín M.D. Ph.D." + }, + "ga/foreignG/sorting": { + "family": { + "expected": "", + "got": "Sinbad" + }, + "given_names": { + "expected": "Sinbad", + "got": "" + }, + "text": "Sinbad" + }, + "ga/foreignG/surnameFirst": { + "family": { + "expected": "", + "got": "Sinbad" + }, + "given_names": { + "expected": "Sinbad", + "got": "" + }, + "text": "Sinbad" + }, + "ga/nativeFull/givenFirst": { + "family_base": { + "expected": "de Bhulbh", + "got": "Bhulbh" + }, + "family_particles": { + "expected": "", + "got": "de" + }, + "given_names": { + "expected": "Brigid Aoibhe Sophia", + "got": "An tOll. An Dr. Brigid Aoibhe Sophia" + }, + "text": "An tOll. An Dr. Brigid Aoibhe Sophia de Bhulbh", + "title": { + "expected": "An tOll. An Dr.", + "got": "" + } + }, + "ga/nativeFull/sorting": { + "family_base": { + "expected": "de Bhulbh", + "got": "Bhulbh" + }, + "family_particles": { + "expected": "", + "got": "de" + }, + "given_names": { + "expected": "Brigid Aoibhe Sophia", + "got": "An tOll. An Dr. Brigid Aoibhe Sophia" + }, + "text": "de Bhulbh An tOll. An Dr. Brigid Aoibhe Sophia", + "title": { + "expected": "An tOll. An Dr.", + "got": "" + } + }, + "ga/nativeFull/surnameFirst": { + "family_base": { + "expected": "de Bhulbh", + "got": "Bhulbh" + }, + "family_particles": { + "expected": "", + "got": "de" + }, + "text": "de Bhulbh Brigid Aoibhe Sophia" + }, + "ga/nativeG/sorting": { + "family": { + "expected": "", + "got": "Hozier" + }, + "given_names": { + "expected": "Hozier", + "got": "" + }, + "text": "Hozier" + }, + "ga/nativeG/surnameFirst": { + "family": { + "expected": "", + "got": "Hozier" + }, + "given_names": { + "expected": "Hozier", + "got": "" + }, + "text": "Hozier" + }, + "gd/foreignFull/givenFirst": { + "surname2": { + "expected": "González Domingo", + "got": "" + }, + "text": "Prof. Dr. Ada Cornelia César Martín von Brühl González Domingo Jr, MD DDS" + }, + "gd/foreignFull/surnameFirst": { + "surname2": { + "expected": "González Domingo", + "got": "" + }, + "text": "von Brühl González Domingo, Prof. Dr. Ada Cornelia César Martín Jr, MD DDS" + }, + "gd/foreignG/sorting": { + "family": { + "expected": "", + "got": "Sinbad" + }, + "given_names": { + "expected": "Sinbad", + "got": "" + }, + "text": "Sinbad" + }, + "gd/foreignG/surnameFirst": { + "family": { + "expected": "", + "got": "Sinbad" + }, + "given_names": { + "expected": "Sinbad", + "got": "" + }, + "text": "Sinbad" + }, + "gd/nativeFull/givenFirst": { + "family_base": { + "expected": "Mac a’ Ghobhainn Chorùna", + "got": "a’ Ghobhainn Chorùna" + }, + "family_particles": { + "expected": "", + "got": "Mac" + }, + "surname2": { + "expected": "Chorùna", + "got": "" + }, + "text": "Mgr. Dòmhnall Iain Bàn Mac a’ Ghobhainn Chorùna am post" + }, + "gd/nativeFull/sorting": { + "family_base": { + "expected": "Mac a’ Ghobhainn", + "got": "a’ Ghobhainn" + }, + "family_particles": { + "expected": "", + "got": "Mac" + }, + "text": "Mac a’ Ghobhainn, Dòmhnall Iain Bàn" + }, + "gd/nativeFull/surnameFirst": { + "family_base": { + "expected": "Mac a’ Ghobhainn Chorùna", + "got": "a’ Ghobhainn Chorùna" + }, + "family_particles": { + "expected": "", + "got": "Mac" + }, + "surname2": { + "expected": "Chorùna", + "got": "" + }, + "text": "Mac a’ Ghobhainn Chorùna, Mgr. Dòmhnall Iain Bàn am post" + }, + "gd/nativeG/sorting": { + "family": { + "expected": "", + "got": "Calum" + }, + "given_names": { + "expected": "Calum", + "got": "" + }, + "text": "Calum" + }, + "gd/nativeG/surnameFirst": { + "family": { + "expected": "", + "got": "Calum" + }, + "given_names": { + "expected": "Calum", + "got": "" + }, + "text": "Calum" + }, "hr/foreignG/sorting": { "family": { "expected": "", @@ -640,6 +865,111 @@ }, "text": "सुन्दर" }, + "pt/foreignFull/givenFirst": { + "suffix": { + "expected": "Jr. Dr. Ph.D", + "got": "Jr., Ph.D" + }, + "text": "Prof. Dr. Mary Lou Cesare Martín von Brühl Jr., Dr. Ph.D", + "title": { + "expected": "Prof. Dr.", + "got": "Prof. Dr. Dr." + } + }, + "pt/foreignFull/surnameFirst": { + "family": { + "expected": "von Brühl", + "got": "von" + }, + "family_base": { + "expected": "Brühl", + "got": "von" + }, + "family_particles": { + "expected": "von", + "got": "" + }, + "given_names": { + "expected": "Mary Lou Cesare Martín", + "got": "Brühl Prof. Dr. Mary Lou Cesare Martín" + }, + "suffix": { + "expected": "Jr. Dr. Ph.D", + "got": "Jr., Ph.D" + }, + "text": "von Brühl Prof. Dr. Mary Lou Cesare Martín Jr., Dr. Ph.D", + "title": { + "expected": "Prof. Dr.", + "got": "Dr." + } + }, + "pt/foreignG/sorting": { + "family": { + "expected": "", + "got": "Sinbad" + }, + "given_names": { + "expected": "Sinbad", + "got": "" + }, + "text": "Sinbad" + }, + "pt/foreignG/surnameFirst": { + "family": { + "expected": "", + "got": "Sinbad" + }, + "given_names": { + "expected": "Sinbad", + "got": "" + }, + "text": "Sinbad" + }, + "pt/nativeFull/surnameFirst": { + "family": { + "expected": "da Silva", + "got": "da" + }, + "family_base": { + "expected": "Silva", + "got": "da" + }, + "family_particles": { + "expected": "da", + "got": "" + }, + "given_names": { + "expected": "José Pedro Eduardo", + "got": "Silva Sr. José Pedro Eduardo" + }, + "text": "da Silva Sr. José Pedro Eduardo Jr., MP", + "title": { + "expected": "Sr.", + "got": "" + } + }, + "pt/nativeG/sorting": { + "family": { + "expected": "", + "got": "Maria" + }, + "given_names": { + "expected": "Maria", + "got": "" + }, + "text": "Maria" + }, + "pt/nativeG/surnameFirst": { + "family": { + "expected": "", + "got": "Maria" + }, + "given_names": { + "expected": "Maria", + "got": "" + }, + "text": "Maria" + }, "ru/foreignFull/givenFirst": { "family": { "expected": "фон Брюль", From 1bf0343603de6d3c89f550f4179e5ca43e03d2f1 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Fri, 30 Oct 2026 03:09:20 -0700 Subject: [PATCH 2/5] Correct the 3h figures the design-docs review found Regenerate the rules corpus and snapshot for rules/S11's new examples, so pt-BR moves 40 committed entries (pt's 38 plus two SB examples); measure ctgov-officials and record the S.B. degree read as Sobrinho's generation under the pack as accepted; state the comparator and population of the tse pair figures, which count pairs, not names. Co-Authored-By: Claude Opus 5.5 --- .../decisions/S11-suffix-word-that-is-also-a-name.md | 2 +- docs/design/decisions/vocabulary-model.md | 8 ++++---- docs/design/mechanisms.md | 2 +- nameparser/vocab/pt-BR.toml | 4 ++-- tools/differential/corpus_rules.jsonl | 3 +++ tools/differential/snapshot.jsonl | 3 +++ 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/design/decisions/S11-suffix-word-that-is-also-a-name.md b/docs/design/decisions/S11-suffix-word-that-is-also-a-name.md index 117c17d90..6fca164b4 100644 --- a/docs/design/decisions/S11-suffix-word-that-is-also-a-name.md +++ b/docs/design/decisions/S11-suffix-word-that-is-also-a-name.md @@ -24,4 +24,4 @@ Open: none in this file. - 2026-10-25 (Derek), #890 (its accepted lower-case `fils` superseded by 2026-10-25 (Derek), #890, THE FLOOR) — A SUFFIX WORD IN FRONT OF AN AGNOMEN IS NO WORD TO SPARE. SUPERSEDES: the ACCEPTED sentence of 2026-10-25 #752 on a generation word or dotted initials in front of an agnomen. Derek approved the fix before merge, on the language-packs session's finding: the entry above accepted that a generation word or credential straight before an agnomen joined it in the suffix, leaving no family name, as a cost of 5 tse names, but that count was of capitals only, and the shape is ordinary in mixed case (`Pedro Junior Filho`, `Pedro Júnior Neto`, `Pedro Jr. Filho`, each read with no family name). One word was counted twice: the count took `Junior` as the agnomen's word to spare and the peel then took `Junior` as a suffix; and an unambiguous suffix word in front made the agnomen a credential by rules/S7's company. A generation word that is also a name now counts only the name words in front of it, skipping what the trailing run takes there (a suffix word, a numeral of two or more letters read as the generation, which the review of the fix found still counted in `Pedro VI Filho`, a member of the ambiguous class its writing makes a suffix, `Pedro MA Filho`, and a bare member any of those carries in front of it, `Pedro Junior Ma Filho` and `Pedro Junior do Filho`, the second review's), and keeps no company, so `Pedro Junior Filho` reads given 'Pedro', middle 'Junior', family 'Filho' as master did, while `Pedro Alves Junior Filho` still reads suffix 'Junior Filho'; dotted initials in front read as master did too (`PEDRO C.M. FILHO` → middle 'C.M.', family 'FILHO'), and so does a credential (`Pedro PhD Neto` → family 'Neto'). The count asks a scan that stops at the first word the walk keeps, not the walk itself: asking the walk over the words in front was tried and is exponential on a run of agnomens, quadratic when memoized, and the scaling guard (`agnomens`, `carried_agnomens` in tests/v2/test_benchmark.py) refuses both. The lower-case `fils` of a mixed-case name is settled by its case and not the count, so `Pedro Junior fils` still reads suffix 'Junior fils' (rules/S11's Accepted). - 2026-10-25 (Derek), #890, THE FLOOR — A NAME KEEPS ITS FAMILY WORD BEFORE A GENERATION WORD'S CASE IS READ. SUPERSEDES: the lower-case `fils` sentence of 2026-10-25 (Derek), #890, and the 2026-10-25 #752 entry's reading of `Dumas fils`. Derek: "apply the floor to fils too". In the gate the agreed design orders (decisions/vocabulary-model, 2026-10-23 #752), the floor, a name keeping a word to address, is the first step and the form cue the second, so case may settle a generation word against the name only where the floor already holds. A lower-case `fils` in a mixed-case name had been taken with no word to spare, as an acronym's capitals are, so `Dumas fils` read given 'Dumas' with no family name and `Pedro Junior fils` suffix 'Junior fils'. Both now read family 'fils', as master did; `Alexandre Dumas fils` and `Pedro Alves Junior fils` keep the suffix, `Marie Fils` her family name, and after a comma, where a family is already named, lower case still settles it (`Dumas, Alexandre fils` → suffix 'fils'). The same holds after a comma with no name before it, the review of the fix found: `, Dr. fils` and `(Pe), Dr. fils` had read suffix 'fils' with no family, the comma decision taking the lower case where no family is named, and read family 'fils' now, as master did; and its second review found the head's name-word count too generous for this, a joined salutation (`Mr. and Mrs., fils`) and a lone name word behind a given-name title (`Sir Alexandre, fils`) counting one where no family remains, so at the first slot after a comma a generation word's lower case adds nothing to rules/C4's count, which reads two name words before the comma first: `Dumas, fils` reads given 'fils', family 'Dumas', as master did, and `Alexandre Dumas, fils` suffix 'fils'. An acronym keeps its capitals' lean with no word to spare (rules/S6, `Jack MA`); an agnomen has no case to weigh, so no other gated word changes. Measured with `--base` against master 225f3d6f: no corpus name moves against the commit before. - 2026-10-27 #843 — THE AGNOMENS' ABBREVIATIONS ARE SPELLINGS OF THE AGNOMEN. Brazilian donors' filings (tse-donors) abbreviate the agnomen where the registry spells it, and #843's last comment counted the filed/registry pairs identical but for that last word (2026-10-25): FILHO as FH 230, FO 99, FILH 17, F 15, FIL 10; NETO as NT 122, NET 12, N 6; SOBRINHO as SOBR 19, SB 11, SOB 4. After #890 the registry's agnomen read as the suffix and the filed abbreviation as the family name, so each such pair read apart. Each abbreviation is now an `agnomen` reading `same_as` its word in `pt-BR.toml`, with a `family` reading (dated counts where `tools/name_usage.py` found bearers, `fo` US Census 238 and `net` 118; a declared one otherwise, as `filha` has), so S11's count gates it exactly as it gates the word. Measured with `tools/differential/measure.py --base` against master 6ae585f3: tse-donors moves 1,555 names (fh 801, nt 492, fo 166, sobr 51, filh 21, net 17, sob 7), and 1,481 filed/registry pairs now read alike, none apart; PSC has no move; elsewhere 6 names move fields, five of them no person's name (an organization's acronym after a particle in four JRC-Names spellings, and an FEC committee name) and one an FEC person's name in surname, given-name order ending in NET, which moves from middle to suffix as `Smith, John NET` does, a cost accepted with S11's (rules/S5 reads the given part's last word by its writing); 17 more gain a report. The words also move the comma shapes, which is what a credential reading of `nt` (13 NPPES providers) would want: `John Smith, NT` read given 'NT', family 'John Smith' on master and reads suffix 'NT' now, and `Smith, John NT` and `Smith, John NET` move from middle to suffix; `John Smith, NET` was a suffix already. Two were measured and refused on the word, with the reason as an `excluded` entry: `fil`, a Slavic and Polish surname (US Census 220), which took two UK PSC filers' surname Fil to the suffix, one a regression, against 11 tse moves; and `sb`, also the S.B. degree that 23 ClinicalTrials.gov officials write, which `capitalized(force=True)` recased to `Sb` and which would read as Sobrinho's spelling. The single letters `F` and `N` are not listed: written exactly as an initial is, nothing in the writing tells them apart, so `JOSE SILVA F` keeps family 'F'. `Fº`, the ordinal spelling, appears in no tse-donors pair (filings are in plain capitals), so it was not listed. -- 2026-10-30 #753, phase 3h — `SB` IS SOBRINHO'S SPELLING UNDER A BRAZILIAN PORTUGUESE CHOICE; `F` AND `N` STAY OUT, THE CHOICE INCLUDED. The 2026-10-27 #843 entry left `sb` refused for its collision with the S.B. degree and `F` and `N` as initials, both for a `pt-BR` pack. `sb` is now an agnomen reading `default = false` in pt-BR.toml, gated by the count as `sobrinho` is, with `cased = "SB"`; `locales.PT_BR` turns it on, and it moved 13 tse-donors names beyond what choosing `pt` moves, the registry spelling SOBRINHO in each. `F` and `N`, turned on the same way, read the letter as the agnomen where the registry abbreviated a surname with it about twice as often as where it wrote the agnomen (`F` 104 to 48, `N` 59 to 18), so each is an `excluded` agnomen with those counts, and `JOSE CARLOS SILVA F` keeps family 'F' under the pack. The measurement and recipe are in decisions/vocabulary-model's 2026-10-30 phase 3h entry. +- 2026-10-30 #753, phase 3h — `SB` IS SOBRINHO'S SPELLING UNDER A BRAZILIAN PORTUGUESE CHOICE; `F` AND `N` STAY OUT, THE CHOICE INCLUDED. The 2026-10-27 #843 entry left `sb` refused for its collision with the S.B. degree and `F` and `N` as initials, both for a `pt-BR` pack. `sb` is now an agnomen reading `default = false` in pt-BR.toml, gated by the count as `sobrinho` is, with `cased = "SB"`; `locales.PT_BR` turns it on, and it moved 13 tse-donors names beyond what choosing `pt` moves, the registry spelling SOBRINHO in each. `F` and `N`, turned on the same way, read the letter as the agnomen where the registry abbreviated a surname with it about twice as often as where it wrote the agnomen (`F` 104 to 48, `N` 59 to 18, filed/registry pairs, each counted under the first letter the reading took), so each is an `excluded` agnomen with those counts, and `JOSE CARLOS SILVA F` keeps family 'F' under the pack. Accepted: under the pack a written S.B. degree is read as Sobrinho too, so `Howard S. Gordon, MD SB` fills `generation` with `SB` (5 ctgov-officials names). The measurement and recipe are in decisions/vocabulary-model's 2026-10-30 phase 3h entry. diff --git a/docs/design/decisions/vocabulary-model.md b/docs/design/decisions/vocabulary-model.md index 46d50fd6b..80b55ae20 100644 --- a/docs/design/decisions/vocabulary-model.md +++ b/docs/design/decisions/vocabulary-model.md @@ -355,7 +355,7 @@ Open: #913 (the title words phase 2j left, read as titles though borne as names) - **2026-10-30 #753, phase 3h — THE `ga`, `gd` AND `pt-BR` PACKS: THE IRISH CLAN FORMS AND `sb` TURN ON; `F` AND `N` STAY OUT, MEASURED; THE DEFAULT DOES NOT MOVE.** Carries out 3h of item 11 of the 2026-10-27 #753 entry, and for `nic` and `sb` the rest of item 9. - **What each pack turns on.** `locales.GA` chooses `ga`, `locales.GD` `gd` and `locales.PT_BR` `pt-BR` (code `pt_br`, its choice selecting `pt` too, item 6 of the phase 3b entry), each with an empty lexicon fragment and no policy patch. ga.toml: `nic` (daughter of), its exclusion converted, with a `given` reading (US SSA 77, Wikidata 173) and a `family` one (US Census 108), so it is a possibly-given particle and a leading `Nic` stays given, reported (rules/P1); `mhic` and `uí`, the married forms of `mac` and `ó`, `may_be_given = false`; `mic`, the genitive of `mac`, with `given` (US SSA 26) and `family` readings (ORCID's family box: Romania 4, Ireland none). Each clan reading is `default = false` and `same_as` its head where it has one. gd.toml: `nic` alone. pt-BR.toml: `sb`, its exclusion converted to an `agnomen` reading `default = false`, `same_as = "sobrinho"`, with a declared `family` reading as its siblings have, and `cased = "SB"`, so the S.B. degree's capitals survive `capitalized(force=True)` under the pack, the recasing that was half of its exclusion's reason. Counts as of 2026-10-30 from `tools/name_usage.py nic mic mhic uí --boxes`. - **`mhic` stays out of gd.toml.** Written in Scottish Gaelic it mostly links a patronymic chain, `Iain mac Dhòmhnaill mhic Sheumais`, which the default reads family `mac Dhòmhnaill mhic Sheumais` only because `mhic` is unlisted; listed, it starts a second particle run and the chain splits at it, middle `mac Dhòmhnaill`, family `mhic Sheumais`, as the default already splits `Iain mac Dhòmhnaill mac Sheumais`. The Irish married form (`Máire Mhic Gabhann`) is the word's modern written use, so ga.toml keeps it and accepts the chain's split (`Seán mac Seáin mhic Thomáis`, pinned nowhere and moving no corpus name). - - **`F` and `N` are excluded, the pack included.** #843 left them to this pack. Turned on, both letters measured on tse-donors (2026-10-30, `snapshot.py --local tse-donors --languages pt-BR --base` against `--languages pt`, each moved name scored by its registry spelling): of the names they moved beyond what `pt` moves, `F` was the agnomen in 48 and a letter abbreviating a surname in 104 (`ANA MARIA F`, registry `ANA MARIA FILGUEIRAS`), `N` in 18 and 59 (`EDINEI VITAL N`, registry `EDINEI VITAL DO NASCIMENTO`), last or before another generation word alike (`ADINALDO VILHENA F JUNIOR`, registry `FERREIRA JUNIOR`). The filings are the evidence the letters were proposed from, and they abbreviate the other words too, so no writing tells the two apart even inside the language: each is an `excluded` agnomen in pt-BR.toml with these counts. `sb` moved 13 names' roles beyond `pt`'s, the registry spelling SOBRINHO in every one (the filed/registry pairs reading alike, 11 under `pt` and the default, rose to 22), and one more name's `capitalized()` view alone, its mask writing a mid-name `SB` (`VIVIANE G A DA SB FERRUGINI`) where the default writes `Sb`. - - **The default does not move.** The build reports `_compiled.py` current; tests/v2/test_snapshot.py passes with no re-record; `snapshot.py --local tse-donors --base` against master `40675146` matches the base. - - **What each choice moves,** measured 2026-10-30 on `40675146` plus this change, Python 3.11. Committed corpora (`snapshot.py --languages TAG`): `ga` and `gd` 4 entries each, every one `mc` (`John Smith MC` family `MC`, where the default reads the Military Cross), the prefer of the phase 3b entry's turning-on bullet on the `mc` both files already held; `pt-BR` the same 38 as `pt` (1 held only by `corpus_cldr.jsonl`), `sb` moving none. CLDR's locale view (`cldr_agreement.py`, `ga`, `gd` and `pt` mapped to the packs, CLDR's `pt` being Brazilian): `ga` 14/24, `gd` 15/24, `pt` 17/24 renderings agreeing with and without the pack, recorded. Fielded registers, scored over every record holding a word the choice changes (`local_agreement.py`'s `read` under each parser): PSC, `ga` 4 fixed (`Clodagh Nic Lochlainn`, `Orlaith Mhic Mhathuna`, and `Damien Patrick Mc` by the `mc` prefer) and 1 broken (`Nicholas Nic Rawlings Limited Rawlings`, a company filed as a person), `gd` 3 and the same 1; ORCID, `ga` 24 fixed and none broken over every country (23 in Ireland and Great Britain, `Pauline Uí Dhuibhir`, `Éimí Mhic an Ridire`), `gd` 20 and none (19 in Ireland and Great Britain). `pt-BR`'s moves there are all `pt`'s: PSC 12 broken, each a trailing `Sr` read as the title Senhor (`Gary Holloway Sr` family `Sr`), ORCID 21 fixed (the one-case `e`) and 11 broken. - - **Open for 3g:** a `pt` choice prefers `pt.toml`'s title `sr` (Senhor) over English's generation, so a trailing `Sr` reads as a title or the family name under `pt` and `pt-BR` (PSC's 12 above); whether the generation survives a Portuguese choice is 3g's, which owns `pt`'s titles. The recipe for the fielded figures is the scratch scorer this entry describes: `local_agreement.read` per record under `Parser()` and `parser_for(pack)`, over the records whose name holds a word of `changed_words(selected(pack.languages))`. + - **`F` and `N` are excluded, the pack included.** #843 left them to this pack. Turned on, both letters measured on tse-donors (2026-10-30, `snapshot.py --local tse-donors --languages pt-BR --base` against `--languages pt`, each moved name scored by its registry spelling): of the filed/registry pairs they moved beyond what `pt` moves (242 pairs, 241 distinct filed names), each scored by whether its registry spells the agnomen and counted under the first letter its reading took as the suffix, `F` was the agnomen in 48 and a letter abbreviating a surname in 104 (`ANA MARIA F`, registry `ANA MARIA FILGUEIRAS`), `N` in 18 and 59 (`EDINEI VITAL N`, registry `EDINEI VITAL DO NASCIMENTO`), last or before another generation word alike (`ADINALDO VILHENA F JUNIOR`, registry `FERREIRA JUNIOR`). The filings are the evidence the letters were proposed from, and they abbreviate the other words too, so no writing tells the two apart even inside the language: each is an `excluded` agnomen in pt-BR.toml with these counts. `sb` moved 13 names' roles beyond `pt`'s, the registry spelling SOBRINHO in every one (the pairs reading alike, of the 6,090 whose filed name holds `f`, `n` or `sb` as a word, rose from 11 under `pt` and the default to 22, alike meaning given, middle, family and suffix each equal with an agnomen letter spelled out as its word), and one more name's `capitalized()` view alone, its mask writing a mid-name `SB` (`VIVIANE G A DA SB FERRUGINI`) where the default writes `Sb`. + - **The default does not move.** The build reports `_compiled.py` current; the committed snapshot gained the three names rules/S11's new examples add to the rules corpus and no existing entry moved; `snapshot.py --local tse-donors --base` against master `40675146` matches the base. + - **What each choice moves,** measured 2026-10-30 on `40675146` plus this change, Python 3.11. Committed corpora (`snapshot.py --languages TAG`): `ga` and `gd` 4 entries each, every one `mc` (`John Smith MC` family `MC`, where the default reads the Military Cross), the prefer of the phase 3b entry's turning-on bullet on the `mc` both files already held; `pt-BR` 40, `pt`'s 38 (1 held only by `corpus_cldr.jsonl`) and the two of rules/S11's new examples that read `SB` (`JOSE CARLOS SILVA SB`, `Ana SB`). CLDR's locale view (`cldr_agreement.py`, `ga`, `gd` and `pt` mapped to the packs, CLDR's `pt` being Brazilian): `ga` 14/24, `gd` 15/24, `pt` 17/24 renderings agreeing with and without the pack, recorded. Fielded registers, scored over every record holding a word the choice changes (`local_agreement.py`'s `read` under each parser): PSC, `ga` 4 fixed (`Clodagh Nic Lochlainn`, `Orlaith Mhic Mhathuna`, and `Damien Patrick Mc` by the `mc` prefer) and 1 broken (`Nicholas Nic Rawlings Limited Rawlings`, a company filed as a person), `gd` 3 and the same 1; ORCID, `ga` 24 fixed and none broken over every country (23 in Ireland and Great Britain, `Pauline Uí Dhuibhir`, `Éimí Mhic an Ridire`), `gd` 20 and none (19 in Ireland and Great Britain). `pt-BR`'s moves there are all `pt`'s: PSC 12 broken, each a trailing `Sr` read as the title Senhor (`Gary Holloway Sr` family `Sr`), ORCID 21 fixed (the one-case `e`) and 11 broken. + - **Open for 3g:** a `pt` choice prefers `pt.toml`'s title `sr` (Senhor) over English's generation, so a trailing `Sr` reads as a title or the family name under `pt` and `pt-BR` (PSC's 12 above); whether the generation survives a Portuguese choice is 3g's, which owns `pt`'s titles. **Accepted: under the pack a written S.B. degree reads as Sobrinho's generation.** In ctgov-officials (`snapshot.py --local ctgov-officials --languages pt-BR --base`, against `--languages pt`), 17 names move beyond `pt`'s: 12 only in `capitalized(force=True)`, a mid-name `SB` keeping its capitals, and 5 where the degree after a comma or a credential now fills `generation` rather than `credentials` (`Howard S. Gordon, MD SB`, `Waleed Riad, MD, AB, SB`), one of them moving fields (`Silvio Brugger, SB`, given `SB` by default, suffix under the pack). The degree is a suffix either way; which view holds it is the cost of a choice made for Brazilian data. The recipe for the fielded figures is the scratch scorer this entry describes: `local_agreement.read` per record under `Parser()` and `parser_for(pack)`, over the records whose name holds a word of `changed_words(selected(pack.languages))`. diff --git a/docs/design/mechanisms.md b/docs/design/mechanisms.md index 7a5d26a8e..c17f4dcf0 100644 --- a/docs/design/mechanisms.md +++ b/docs/design/mechanisms.md @@ -430,7 +430,7 @@ A second axis cuts across the jobs: whether the names are real or invented, labe **How it works.** Agreement is not correctness, so this is a second measure beside a SCORE (mechanisms.md#NAME-DATA-JOBS), never in place of one. Its trap is a comparison that agreed for the wrong reason. #752's first version compared the family's LAST WORD as written, and gating the Brazilian agnomens counted 1,043 tse pairs "now apart" (2026-10-24): on master both spellings had agreed on `FILHO` as the family, and once the agnomen read as a suffix the comparison reached the real surname, which the two spellings write differently. Comparing whole fields after folding cut the agnomen gate's apart count to 304, and reading those by shape showed what was left: a word differing between the spellings, an initial, a length difference, and 47 differing only by accent or case, which point at the one real defect: the spike asked a case cue the design does not. Across scripts the same measure found what no single-script corpus could: taking `Guru` out of the titles pushed 44 Nepali pairs apart because its Devanagari twin stayed a title. -**Reach for it when.** A change moves a word between fields in a population that holds the same names spelled twice; and when a vocabulary word has a twin in another script, which a pair source shows and a single-script snapshot cannot. And when a filed spelling ABBREVIATES its twin's words, so that most moved pairs read apart before and after: alike-or-apart then counts only the moves that land exactly, and a move from one wrong reading to another is silent. #753's phase 3h counted `pt-BR`'s `F`, `N` and `SB` at 34 pairs fixed and none broken by that measure, while 208 moved pairs read apart both ways; asked what the registry wrote at the moved word (`F` beside `FILHO`, or `F` beside a surname), 163 of all 242 moves were a letter abbreviating a surname, and `F` and `N` stayed out (2026-10-30). Score each move by the twin's word at the place the change acts, not only by whole-name agreement. +**Reach for it when.** A change moves a word between fields in a population that holds the same names spelled twice; and when a vocabulary word has a twin in another script, which a pair source shows and a single-script snapshot cannot. And when a filed spelling ABBREVIATES its twin's words, so that most moved pairs read apart before and after: alike-or-apart then counts only the moves that land exactly, and a move from one wrong reading to another is silent. #753's phase 3h counted `pt-BR`'s `F`, `N` and `SB` at 34 pairs fixed and none broken by that measure (given, middle, family and suffix each equal, a letter spelled out as its agnomen), while 208 moved pairs read apart both ways; asked what the registry wrote at the moved word (`F` beside `FILHO`, or `F` beside a surname), 163 of the 242 moved pairs were a letter abbreviating a surname, and `F` and `N` stayed out (2026-10-30). Score each move by the twin's word at the place the change acts, not only by whole-name agreement. ### Field notes — the traps themselves diff --git a/nameparser/vocab/pt-BR.toml b/nameparser/vocab/pt-BR.toml index aeb72d250..78f9c7936 100644 --- a/nameparser/vocab/pt-BR.toml +++ b/nameparser/vocab/pt-BR.toml @@ -2,7 +2,7 @@ schema = 1 [[word]] text = "f" -excluded = { agnomen = { reason = "Filho written short in 15 tse-donors filed/registry pairs (2026-10-25), but written exactly as an initial is, and in those filings mostly one: turned on by a Brazilian Portuguese choice it moved 152 tse-donors names (measured with 'n' on too) that the choice of Portuguese alone does not, and in 104 of them the registry spells a surname the letter abbreviates (ANA MARIA F, registry ANA MARIA FILGUEIRAS) where in 48 it spells FILHO (2026-10-30). No writing tells the two apart, so no pack reads it", issue = [843, 753], decided = 2026-10-30 } } +excluded = { agnomen = { reason = "Filho written short in 15 tse-donors filed/registry pairs (2026-10-25), but written exactly as an initial is, and in those filings mostly one: turned on by a Brazilian Portuguese choice it moved 152 tse-donors filed/registry pairs (measured with 'n' on too, a pair holding both counted under the letter its reading took first) that the choice of Portuguese alone does not, and in 104 of them the registry spells a surname the letter abbreviates (ANA MARIA F, registry ANA MARIA FILGUEIRAS) where in 48 it spells FILHO (2026-10-30). No writing tells the two apart, so no pack reads it", issue = [843, 753], decided = 2026-10-30 } } [[word]] text = "fil" @@ -25,7 +25,7 @@ family = { us_census_2010 = 238, wikidata = 6, as_of = 2026-10-27, note = "Itali [[word]] text = "n" -excluded = { agnomen = { reason = "Neto written short in 6 tse-donors filed/registry pairs (2026-10-25), but written exactly as an initial is, and in those filings mostly one: turned on by a Brazilian Portuguese choice it moved 77 tse-donors names (measured with 'f' on too) that the choice of Portuguese alone does not, and in 59 of them the registry spells a surname the letter abbreviates (EDINEI VITAL N, registry EDINEI VITAL DO NASCIMENTO) where in 18 it spells NETO (2026-10-30). No writing tells the two apart, so no pack reads it", issue = [843, 753], decided = 2026-10-30 } } +excluded = { agnomen = { reason = "Neto written short in 6 tse-donors filed/registry pairs (2026-10-25), but written exactly as an initial is, and in those filings mostly one: turned on by a Brazilian Portuguese choice it moved 77 tse-donors filed/registry pairs (measured with 'f' on too) that the choice of Portuguese alone does not, and in 59 of them the registry spells a surname the letter abbreviates (EDINEI VITAL N, registry EDINEI VITAL DO NASCIMENTO) where in 18 it spells NETO (2026-10-30). No writing tells the two apart, so no pack reads it", issue = [843, 753], decided = 2026-10-30 } } [[word]] text = "net" diff --git a/tools/differential/corpus_rules.jsonl b/tools/differential/corpus_rules.jsonl index 807473b9e..c4e74507e 100644 --- a/tools/differential/corpus_rules.jsonl +++ b/tools/differential/corpus_rules.jsonl @@ -71,6 +71,7 @@ "Ana María van der Berg" "Ana María-José López" "Ana Neto" +"Ana SB" "Ana abdul Rahman López" "Andrew" "Andrew (Andy) Perkins" @@ -315,6 +316,8 @@ "JOHN SMITH, PU-PH" "JOHN SMITH, VD MA" "JOHN V" +"JOSE CARLOS SILVA F" +"JOSE CARLOS SILVA SB" "JOSE DA SILVA FH" "JOSE ORTEGA-Y-GASSET" "JOSÉ GARCÍA" diff --git a/tools/differential/snapshot.jsonl b/tools/differential/snapshot.jsonl index 5491a1bcb..564381280 100644 --- a/tools/differential/snapshot.jsonl +++ b/tools/differential/snapshot.jsonl @@ -213,6 +213,7 @@ {"name": "Ana María van der Berg", "given": "Ana", "middle": "María", "family": "van der Berg", "initials": "A. M. B."} {"name": "Ana María-José López", "given": "Ana", "middle": "María-José", "family": "López", "initials": "A. M. L."} {"name": "Ana Neto", "given": "Ana", "family": "Neto", "ambiguities": ["SUFFIX_OR_NAME"], "alternatives": ["SUFFIX_OR_NAME: agnomen as suffix (vocabulary)"], "initials": "A. N."} +{"name": "Ana SB", "given": "Ana", "family": "SB", "initials": "A. S.", "capitalized_force": "Ana Sb"} {"name": "Ana Torres", "given": "Ana", "family": "Torres", "initials": "A. T."} {"name": "Ana abdul Rahman López", "given": "Ana", "middle": "abdul Rahman", "family": "López", "initials": "A. a. R. L.", "capitalized_force": "Ana Abdul Rahman López"} {"name": "Anan ben David", "given": "Anan", "middle": "ben", "family": "David", "initials": "A. b. D.", "capitalized_force": "Anan Ben David"} @@ -1445,6 +1446,8 @@ {"name": "JOHN SMITH, PU-PH", "given": "PU-PH", "family": "JOHN SMITH", "initials": "P. J. S.", "capitalized": "Pu-Ph John Smith"} {"name": "JOHN SMITH, VD MA", "given": "JOHN", "family": "SMITH", "suffix": "VD MA", "ambiguities": ["SUFFIX_OR_NAME"], "alternatives": ["SUFFIX_OR_NAME: name as given (position)"], "initials": "J. S.", "capitalized": "John Smith, VD MA"} {"name": "JOHN V", "given": "JOHN", "family": "V", "ambiguities": ["SUFFIX_OR_NAME"], "alternatives": ["SUFFIX_OR_NAME: generation as suffix (vocabulary)"], "initials": "J. V.", "capitalized": "John V"} +{"name": "JOSE CARLOS SILVA F", "given": "JOSE", "middle": "CARLOS SILVA", "family": "F", "initials": "J. C. S. F.", "capitalized": "Jose Carlos Silva F"} +{"name": "JOSE CARLOS SILVA SB", "given": "JOSE", "middle": "CARLOS SILVA", "family": "SB", "initials": "J. C. S. S.", "capitalized": "Jose Carlos Silva Sb"} {"name": "JOSE DA SILVA FH", "given": "JOSE", "family": "DA SILVA", "suffix": "FH", "ambiguities": ["SUFFIX_OR_NAME"], "alternatives": ["SUFFIX_OR_NAME: name as family (declared)"], "initials": "J. S.", "capitalized": "Jose da Silva Fh", "generation": "FH"} {"name": "JOSE E MARIA SANTOS", "given": "JOSE", "middle": "E MARIA", "family": "SANTOS", "ambiguities": ["CONJUNCTION_OR_INITIAL"], "alternatives": ["CONJUNCTION_OR_INITIAL: conjunction as given (vocabulary)"], "initials": "J. E. M. S.", "capitalized": "Jose E Maria Santos"} {"name": "JOSE ORTEGA-Y-GASSET", "given": "JOSE", "family": "ORTEGA-Y-GASSET", "initials": "J. O.", "capitalized": "Jose Ortega-y-Gasset"} From dfae03142a102fb0a831ded359b3aad58878584b Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Fri, 30 Oct 2026 03:22:44 -0700 Subject: [PATCH 3/5] Address the 3h review: Irish case masks, costs stated in the docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nic, Mhic and Mic carry their title case as cased, as ní and uí do: without a mask a turned-on particle is lowercased by case repair, so MÁIRE NIC GIOLLA PHÁDRAIG repaired to 'Máire nic' under ga and gd. Rows pin repair and force under each pack. docs/locales.rst states the MC, Sr and S.B. readings as costs, and a leading Nic as given only in a name read given-first; the SB mask's reach, the F/N recipe tree and rules/S11's and decisions/S11's wording are corrected; the release bullet is trimmed to one example. Co-Authored-By: Claude Opus 5.5 --- .../S11-suffix-word-that-is-also-a-name.md | 2 +- docs/design/decisions/vocabulary-model.md | 4 +-- .../S11-suffix-word-that-is-also-a-name.md | 2 +- docs/locales.rst | 28 +++++++++++++------ docs/release_log.rst | 2 +- nameparser/config/_index.py | 6 ++-- nameparser/locales/ga.py | 3 +- nameparser/locales/gd.py | 4 +-- nameparser/vocab/AGENTS.md | 4 +++ nameparser/vocab/ga.toml | 9 ++++-- nameparser/vocab/gd.toml | 3 +- nameparser/vocab/pt-BR.toml | 2 +- tests/v2/rules_budget.json | 2 +- tests/v2/test_locales.py | 19 +++++++++++++ 14 files changed, 64 insertions(+), 26 deletions(-) diff --git a/docs/design/decisions/S11-suffix-word-that-is-also-a-name.md b/docs/design/decisions/S11-suffix-word-that-is-also-a-name.md index 6fca164b4..6178185f8 100644 --- a/docs/design/decisions/S11-suffix-word-that-is-also-a-name.md +++ b/docs/design/decisions/S11-suffix-word-that-is-also-a-name.md @@ -10,7 +10,7 @@ - The floor holds before a generation word's case: a lower-case `fils` in a mixed-case name is the generation only where the name keeps a family word, so `Dumas fils` and `Pedro Junior fils` read family 'fils' — 2026-10-25 (Derek), #890, THE FLOOR. -- The abbreviations Brazilian filings write for the agnomens are agnomen spellings `same_as` their word, gated as it is: `fh`, `fo`, `filh` (filho), `net`, `nt` (neto), `sob`, `sobr` (sobrinho), each with a `family` reading, counted or declared, so that a name of two words keeps it (`Ana Fo` → family 'Fo'); `fil` is refused on the word, `sb` is read only under a Brazilian Portuguese choice, and the single letters `F` and `N` are left as initials, under that choice too — 2026-10-27 #843; 2026-10-30 #753, phase 3h. +- The abbreviations Brazilian filings write for the agnomens are agnomen spellings `same_as` their word, gated as it is: `fh`, `fo`, `filh` (filho), `net`, `nt` (neto), `sob`, `sobr` (sobrinho), each with a `family` reading, counted or declared, so that a name of two words keeps it (`Ana Fo` → family 'Fo'); `fil` is refused on the word, `sb` is read only under a Brazilian Portuguese choice, and the single letters `F` and `N` are left to read as the name word they are written as, an initial or, ending the name, the family (`JOSE CARLOS SILVA F` → family 'F'), under that choice too — 2026-10-27 #843; 2026-10-30 #753, phase 3h. Open: none in this file. diff --git a/docs/design/decisions/vocabulary-model.md b/docs/design/decisions/vocabulary-model.md index 80b55ae20..cbf5a26d1 100644 --- a/docs/design/decisions/vocabulary-model.md +++ b/docs/design/decisions/vocabulary-model.md @@ -353,9 +353,9 @@ Open: #913 (the title words phase 2j left, read as titles though borne as names) - **Found, not changed:** a gated generation behind a particle run after a family comma stays in the given part unreported, `Novák, Freiherr von Berg st.` reading given `von Berg st.` under these packs as the default reads `Dumas, Freiherr von Berg fils` (rules/S11); the default reading of the same `st.` is stranger, family `st. Novák`. Pinned in the 3f table. And with one word before a family comma, `Novák, st.` reads title `st.` by its period shape (rules/H2) under all four packs, reporting suffix-or-name with the generation as the alternative though the reading taken is a title, as the default reads `Dumas, fils.`; the ungated `Novák, ml.` reads suffix `ml.`. `Novák st., Jan` keeps family `Novák st.`, as the default keeps `Dumas fils, Alexandre`: the gated word before a family comma is a name word. Both pinned in the 3f table. - **Open:** `pan`, kept because the work order names it: the corpora show its cost (81 Chinese names across ctgov, ORCID, PSC and TSE read with the title) and no gain, the shape that excluded `bc`, and as a title on its own it also takes a lone given name after a family comma (`Smith, Pan` reads title `Pan` with no given name, a bare `Pan` title `Pan`), so whether it stays in the Czech pack is a question for the reviewer. Whether the uncontested titles (`mudr`, `judr`, `phdr`, `rndr` and the rest, no name reading and no clash found) belong in the default rather than a pack is a default change, measured on ctgov, outside phase 3's rule that nothing moves the default. - **2026-10-30 #753, phase 3h — THE `ga`, `gd` AND `pt-BR` PACKS: THE IRISH CLAN FORMS AND `sb` TURN ON; `F` AND `N` STAY OUT, MEASURED; THE DEFAULT DOES NOT MOVE.** Carries out 3h of item 11 of the 2026-10-27 #753 entry, and for `nic` and `sb` the rest of item 9. - - **What each pack turns on.** `locales.GA` chooses `ga`, `locales.GD` `gd` and `locales.PT_BR` `pt-BR` (code `pt_br`, its choice selecting `pt` too, item 6 of the phase 3b entry), each with an empty lexicon fragment and no policy patch. ga.toml: `nic` (daughter of), its exclusion converted, with a `given` reading (US SSA 77, Wikidata 173) and a `family` one (US Census 108), so it is a possibly-given particle and a leading `Nic` stays given, reported (rules/P1); `mhic` and `uí`, the married forms of `mac` and `ó`, `may_be_given = false`; `mic`, the genitive of `mac`, with `given` (US SSA 26) and `family` readings (ORCID's family box: Romania 4, Ireland none). Each clan reading is `default = false` and `same_as` its head where it has one. gd.toml: `nic` alone. pt-BR.toml: `sb`, its exclusion converted to an `agnomen` reading `default = false`, `same_as = "sobrinho"`, with a declared `family` reading as its siblings have, and `cased = "SB"`, so the S.B. degree's capitals survive `capitalized(force=True)` under the pack, the recasing that was half of its exclusion's reason. Counts as of 2026-10-30 from `tools/name_usage.py nic mic mhic uí --boxes`. + - **What each pack turns on.** `locales.GA` chooses `ga`, `locales.GD` `gd` and `locales.PT_BR` `pt-BR` (code `pt_br`, its choice selecting `pt` too, item 6 of the phase 3b entry), each with an empty lexicon fragment and no policy patch. ga.toml: `nic` (daughter of), its exclusion converted, with a `given` reading (US SSA 77, Wikidata 173) and a `family` one (US Census 108), so it is a possibly-given particle and a leading `Nic` stays given, reported (rules/P1); `mhic` and `uí`, the married forms of `mac` and `ó`, `may_be_given = false`; `mic`, the genitive of `mac`, with `given` (US SSA 26) and `family` readings (ORCID's family box: Romania 4, Ireland none). Each clan reading is `default = false` and `same_as` its head where it has one. gd.toml: `nic` alone. pt-BR.toml: `sb`, its exclusion converted to an `agnomen` reading `default = false`, `same_as = "sobrinho"`, with a declared `family` reading as its siblings have, and `cased = "SB"`, so the S.B. degree's capitals survive `capitalized(force=True)` under the pack, the recasing that was half of its exclusion's reason; the mask is the word's, so Sobrinho's spelling recases to it too (`José Silva Sb.` force-repairs to `SB.`). Irish `nic`, `mhic` and `mic` carry their title case as `cased` too, as `ní` and `uí` do: a particle reading without one is lowercased by case repair (rules/R4), so `MÁIRE NIC GIOLLA PHÁDRAIG` repaired to `Máire nic Giolla Phádraig` under the pack, where the default gives `Nic` (found in review). Counts as of 2026-10-30 from `tools/name_usage.py nic mic mhic uí --boxes`. - **`mhic` stays out of gd.toml.** Written in Scottish Gaelic it mostly links a patronymic chain, `Iain mac Dhòmhnaill mhic Sheumais`, which the default reads family `mac Dhòmhnaill mhic Sheumais` only because `mhic` is unlisted; listed, it starts a second particle run and the chain splits at it, middle `mac Dhòmhnaill`, family `mhic Sheumais`, as the default already splits `Iain mac Dhòmhnaill mac Sheumais`. The Irish married form (`Máire Mhic Gabhann`) is the word's modern written use, so ga.toml keeps it and accepts the chain's split (`Seán mac Seáin mhic Thomáis`, pinned nowhere and moving no corpus name). - - **`F` and `N` are excluded, the pack included.** #843 left them to this pack. Turned on, both letters measured on tse-donors (2026-10-30, `snapshot.py --local tse-donors --languages pt-BR --base` against `--languages pt`, each moved name scored by its registry spelling): of the filed/registry pairs they moved beyond what `pt` moves (242 pairs, 241 distinct filed names), each scored by whether its registry spells the agnomen and counted under the first letter its reading took as the suffix, `F` was the agnomen in 48 and a letter abbreviating a surname in 104 (`ANA MARIA F`, registry `ANA MARIA FILGUEIRAS`), `N` in 18 and 59 (`EDINEI VITAL N`, registry `EDINEI VITAL DO NASCIMENTO`), last or before another generation word alike (`ADINALDO VILHENA F JUNIOR`, registry `FERREIRA JUNIOR`). The filings are the evidence the letters were proposed from, and they abbreviate the other words too, so no writing tells the two apart even inside the language: each is an `excluded` agnomen in pt-BR.toml with these counts. `sb` moved 13 names' roles beyond `pt`'s, the registry spelling SOBRINHO in every one (the pairs reading alike, of the 6,090 whose filed name holds `f`, `n` or `sb` as a word, rose from 11 under `pt` and the default to 22, alike meaning given, middle, family and suffix each equal with an agnomen letter spelled out as its word), and one more name's `capitalized()` view alone, its mask writing a mid-name `SB` (`VIVIANE G A DA SB FERRUGINI`) where the default writes `Sb`. + - **`F` and `N` are excluded, the pack included.** #843 left them to this pack. Turned on, both letters measured on tse-donors (2026-10-30, `snapshot.py --local tse-donors --languages pt-BR --base` against `--languages pt`, each moved name scored by its registry spelling, on a tree with `f` and `n` written as `agnomen` readings `default = false`, `same_as = "filho"` and `"neto"` with a declared `family` reading, as `sb` is, and rebuilt): of the filed/registry pairs they moved beyond what `pt` moves (242 pairs, 241 distinct filed names), each scored by whether its registry spells the agnomen and counted under the first letter its reading took as the suffix, `F` was the agnomen in 48 and a letter abbreviating a surname in 104 (`ANA MARIA F`, registry `ANA MARIA FILGUEIRAS`), `N` in 18 and 59 (`EDINEI VITAL N`, registry `EDINEI VITAL DO NASCIMENTO`), last or before another generation word alike (`ADINALDO VILHENA F JUNIOR`, registry `FERREIRA JUNIOR`). The filings are the evidence the letters were proposed from, and they abbreviate the other words too, so no writing tells the two apart even inside the language: each is an `excluded` agnomen in pt-BR.toml with these counts. `sb` moved 13 names' roles beyond `pt`'s, the registry spelling SOBRINHO in every one (the pairs reading alike, of the 6,090 whose filed name holds `f`, `n` or `sb` as a word, rose from 11 under `pt` and the default to 22, alike meaning given, middle, family and suffix each equal with an agnomen letter spelled out as its word), and one more name's `capitalized()` view alone, its mask writing a mid-name `SB` (`VIVIANE G A DA SB FERRUGINI`) where the default writes `Sb`. - **The default does not move.** The build reports `_compiled.py` current; the committed snapshot gained the three names rules/S11's new examples add to the rules corpus and no existing entry moved; `snapshot.py --local tse-donors --base` against master `40675146` matches the base. - **What each choice moves,** measured 2026-10-30 on `40675146` plus this change, Python 3.11. Committed corpora (`snapshot.py --languages TAG`): `ga` and `gd` 4 entries each, every one `mc` (`John Smith MC` family `MC`, where the default reads the Military Cross), the prefer of the phase 3b entry's turning-on bullet on the `mc` both files already held; `pt-BR` 40, `pt`'s 38 (1 held only by `corpus_cldr.jsonl`) and the two of rules/S11's new examples that read `SB` (`JOSE CARLOS SILVA SB`, `Ana SB`). CLDR's locale view (`cldr_agreement.py`, `ga`, `gd` and `pt` mapped to the packs, CLDR's `pt` being Brazilian): `ga` 14/24, `gd` 15/24, `pt` 17/24 renderings agreeing with and without the pack, recorded. Fielded registers, scored over every record holding a word the choice changes (`local_agreement.py`'s `read` under each parser): PSC, `ga` 4 fixed (`Clodagh Nic Lochlainn`, `Orlaith Mhic Mhathuna`, and `Damien Patrick Mc` by the `mc` prefer) and 1 broken (`Nicholas Nic Rawlings Limited Rawlings`, a company filed as a person), `gd` 3 and the same 1; ORCID, `ga` 24 fixed and none broken over every country (23 in Ireland and Great Britain, `Pauline Uí Dhuibhir`, `Éimí Mhic an Ridire`), `gd` 20 and none (19 in Ireland and Great Britain). `pt-BR`'s moves there are all `pt`'s: PSC 12 broken, each a trailing `Sr` read as the title Senhor (`Gary Holloway Sr` family `Sr`), ORCID 21 fixed (the one-case `e`) and 11 broken. - **Open for 3g:** a `pt` choice prefers `pt.toml`'s title `sr` (Senhor) over English's generation, so a trailing `Sr` reads as a title or the family name under `pt` and `pt-BR` (PSC's 12 above); whether the generation survives a Portuguese choice is 3g's, which owns `pt`'s titles. **Accepted: under the pack a written S.B. degree reads as Sobrinho's generation.** In ctgov-officials (`snapshot.py --local ctgov-officials --languages pt-BR --base`, against `--languages pt`), 17 names move beyond `pt`'s: 12 only in `capitalized(force=True)`, a mid-name `SB` keeping its capitals, and 5 where the degree after a comma or a credential now fills `generation` rather than `credentials` (`Howard S. Gordon, MD SB`, `Waleed Riad, MD, AB, SB`), one of them moving fields (`Silvio Brugger, SB`, given `SB` by default, suffix under the pack). The degree is a suffix either way; which view holds it is the cost of a choice made for Brazilian data. The recipe for the fielded figures is the scratch scorer this entry describes: `local_agreement.read` per record under `Parser()` and `parser_for(pack)`, over the records whose name holds a word of `changed_words(selected(pack.languages))`. diff --git a/docs/design/rules/S11-suffix-word-that-is-also-a-name.md b/docs/design/rules/S11-suffix-word-that-is-also-a-name.md index bb40e3d82..947d896fa 100644 --- a/docs/design/rules/S11-suffix-word-that-is-also-a-name.md +++ b/docs/design/rules/S11-suffix-word-that-is-also-a-name.md @@ -4,7 +4,7 @@ **Then:** it reads as the generation only where the name has words to spare, as a bare ambiguous acronym does (S4), and at the slots that report, either reading carries the ambiguity flag. -- **The writing is read by S6**, and for a generation word such as French `fils` it speaks at the other end of the case: lower case in a name of mixed case is the generation, Title case leans to the name, capitals say nothing. The floor holds first: lower case settles it only where the name keeps a family word, so two name words keep the family, and at the first slot after a comma the count of name words before it decides alone (C4). An agnomen, the Portuguese `Filho`, `Neto`, `Sobrinho` and their feminines, is written in the case of the name around it, so its writing never speaks and the count decides alone; so is each abbreviation of one that Brazilian filings write (`FH`, `Fo`, `Nt`, `Sobr`), a spelling of the word it shortens that takes the word's name reading with it, borne or not. Under a Brazilian Portuguese choice so is `SB`, which by default reads as the name word the S.B. degree is. +- **The writing is read by S6**, and for a generation word such as French `fils` it speaks at the other end of the case: lower case in a name of mixed case is the generation, Title case leans to the name, capitals say nothing. The floor holds first: lower case settles it only where the name keeps a family word, so two name words keep the family, and at the first slot after a comma the count of name words before it decides alone (C4). An agnomen, the Portuguese `Filho`, `Neto`, `Sobrinho` and their feminines, is written in the case of the name around it, so its writing never speaks and the count decides alone; so is each abbreviation of one that Brazilian filings write (`FH`, `Fo`, `Nt`, `Sobr`), a spelling of the word it shortens that takes the word's name reading with it, borne or not. Under a Brazilian Portuguese choice so is `SB`, which by default reads as a name word (the family). - **Only name words in front of it count**: a word there that the trailing run takes, a suffix word, a roman numeral read as the generation (S10), a member of the ambiguous class its writing makes a suffix (S6) or one such a word carries in front of it (S7), is no word to spare, and a suffix word there is no company either, as it is for an acronym (S7). - **After a comma** the word is read as a member of the ambiguous class is: by the count of the name words before the comma at the first slot after it (C4), and by its writing alone where it ends the given part after a family comma (S5). diff --git a/docs/locales.rst b/docs/locales.rst index b0694a156..726d04852 100644 --- a/docs/locales.rst +++ b/docs/locales.rst @@ -349,7 +349,8 @@ Shipped packs default leaves off: ``Nic`` (daughter of), the married forms ``Mhic`` and ``Uí``, and ``Mic`` (``Máire Uí Bhriain`` → family ``Uí Bhriain``). ``Nic`` and ``Mic`` are also given names, so - one opening the name stays the given name, reported. See + one opening a name read given-first stays the given name, + reported. See `Packs that choose a language`_. * - ``gd`` - Scottish Gaelic — chooses ``gd``, which turns on ``Nic`` @@ -497,14 +498,23 @@ none of them, and ``ga``, ``gd`` and ``pt_br`` turn them on: Each word is then read as its language reads the full word: ``SB`` is a suffix only where the name keeps a family name without it (``Ana SB`` -keeps family ``SB``), and ``Nic`` opening a name stays the given name -(``Nic Cage``). Each reports its doubt in -``ambiguities``. A choice also settles every clash its language has a -reading for, as `Choosing languages`_ shows: under ``ga`` and ``gd``, -``Mc`` is the clan particle and not the Military Cross -(``John Smith MC`` → family ``MC``), and ``pt_br`` reads as ``pt`` -does (``John Smith DO`` → family ``DO``, ``J. Smith Sr.`` → title -``Sr.``, Senhor). +keeps family ``SB``), and in a name read given-first ``Nic`` opening it +stays the given name (``Nic Cage``), as ``Mac`` does by default; under +a family-first order it is the first word, read as the family name. +Each reports its doubt in ``ambiguities``. + +A choice also settles every clash its language has a reading for, as +`Choosing languages`_ shows, and that costs the other languages' +readings of those words: + +- **Under ga and gd,** ``Mc`` is the clan particle, so ``John Smith MC`` + gives family ``MC`` and loses the Military Cross. +- **Under pt_br,** the words read as ``pt`` reads them: ``John Smith DO`` + gives family ``DO``, and a trailing ``Sr`` is read as the Portuguese + title *Senhor* rather than English "Senior", so ``John Smith Sr`` + gives family ``Sr``, a known cost not yet settled. +- **Under pt_br,** a written S.B. degree reads as Sobrinho + (``Howard Gordon, MD SB`` fills ``generation`` with ``SB``). .. _segmenter-contract: diff --git a/docs/release_log.rst b/docs/release_log.rst index 11c0ddc41..37177aa4e 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -8,7 +8,7 @@ Release Log - **Add Locale.languages, a choice of languages for the default vocabulary.** ``parser_for(Locale(code="pt", lexicon=Lexicon.empty(), languages=frozenset({"pt"}))).parse("ANA SILVA E SOUZA")`` gives family ``SILVA E SOUZA``, where the default reads family ``SOUZA``, the ``E`` an initial: a chosen language's reading of a word it has a vocabulary reading of wins, and the other languages' vocabulary readings of that word leave, while its readings as a name and the readings shared by every language stay, so under ``pt`` ``John Smith DO`` reads family ``DO``. A generation word another language collides with is read only under its own language: ``Jiří Dienstbier ml.`` gives suffix ``ml.`` under ``cs``, and the Czech, Slovak, Slovenian and Croatian ``ml.`` and ``st.`` and the French ``père`` read this way. A pack choosing no language parses as before. See :doc:`locales`. :issue:`753` - **Add locales.NE, a Nepali pack, the first that is a choice of languages.** ``parser_for(locales.NE).parse("Guru Prasad Sharma")`` gives given ``Guru``, middle ``Prasad``, where the default parser gives title ``Guru``, given ``Prasad``; ``श्री`` and ``गुरु`` read the same way, each a name word placed by position (the family name under a family-first order) and still a title inside a title phrase (``His Holiness Guru Prasad``). It reads Hindi and Marathi names too, where ``श्री`` is usually the salutation, so use it for Nepali data. :issue:`753` - - **Add the Irish, Scottish Gaelic and Brazilian Portuguese packs, locales.GA, locales.GD and locales.PT_BR.** ``parser_for(locales.GA).parse("Máire Uí Bhriain")`` gives family ``Uí Bhriain``, where the default, as in 1.4.0 through 2.8.0, gives middle ``Uí``. ``GA`` reads ``Nic``, ``Mhic``, ``Mic`` and ``Uí`` as clan particles and ``GD`` ``Nic``, a leading ``Nic`` staying the given name; ``PT_BR`` reads ``SB`` as the agnomen Sobrinho, while ``F`` and ``N`` stay the name they spell. Each is a choice of languages, which settles its language's clashes its way: ``John Smith MC`` gives family ``MC`` under ``GA`` and ``GD``, and ``John Smith DO`` family ``DO`` under ``PT_BR``. See :doc:`locales`. :issue:`753` + - **Add the Irish, Scottish Gaelic and Brazilian Portuguese packs, locales.GA, locales.GD and locales.PT_BR.** ``parser_for(locales.GA).parse("Máire Uí Bhriain")`` gives family ``Uí Bhriain``, where the default, as in 1.4.0 through 2.8.0, gives middle ``Uí``. ``GA`` reads ``Nic``, ``Mhic``, ``Mic`` and ``Uí`` as surname particles, ``GD`` reads ``Nic``, and ``PT_BR`` reads ``SB`` as the agnomen Sobrinho; ``F`` and ``N`` stay the name they spell. A choice settles its language's clashes its way, at a cost: ``John Smith MC`` loses the Military Cross under ``GA`` and ``GD``. See :doc:`locales`. :issue:`753` - **Add ParsedName.generation and ParsedName.credentials, the suffix split by kind.** ``parse("Smith, John, Jr., MD, Esq.")`` gives generation ``Jr.`` and credentials ``MD, Esq.``, where 1.4.0 through 2.8.0 gave only suffix ``Jr., MD, Esq.``, which is unchanged. They are views like ``family_base``: render keys, not fields of ``as_dict()``. An honorific after a name (``Esq.``, ``さん``) is a credential, and a suffix written by ``replace()``, or in a name unpickled from 2.x, is all credentials. ``Token.reading`` reads ``Jr.`` and ``Sr.`` as a generation, and ``Ambiguity.alternative`` of a lone ``Jr`` names a generation where 2.7.0 and 2.8.0 named a post-nominal. :issue:`326`, :issue:`754` - **Add ParsedName.patronymic, the words naming the bearer's parent.** ``parse("Mahathir bin Mohamad")`` gives patronymic ``bin Mohamad`` beside family ``bin Mohamad``, which 1.4.0 through 2.8.0 gave alone and which is unchanged; under ``locales.RU``, ``Ivan Ivanovich Ivanov`` gives patronymic ``Ivanovich`` and keeps it as the middle name, and under ``locales.TR_AZ`` a marker such as ``oglu`` reads with the name before it. A view like ``family_base``, not a field of ``as_dict()``, read only from a patronymic particle (``bin``, ``binti``, ``binte``, ``ibn``, ``bint``, ``bar``, ``bat`` and their Arabic and Hebrew forms) or a rule a locale turns on, so ``Anwar Ibrahim`` has none, and a name written by ``replace()``, or unpickled from 2.x, has none either. ``Ambiguity.alternative`` of a leading ``bin`` read as a given name names a patronymic where 2.7.0 and 2.8.0 named a particle. :issue:`754` - **Add ParsedName.surname2, the second of two family names.** ``Parser(policy=Policy(two_family_names=True)).parse("Gabriel García Márquez")`` gives surname2 ``Márquez`` beside family ``García Márquez``, which 2.8.0 gave alone and which is unchanged. A view like ``family_base``, not a field of ``as_dict()``, filled only where ``two_family_names`` reads the family as two, so the default, a family comma (``Lloyd Webber, Andrew``) and a hyphenated family have none, and so does a name unpickled from 2.x or one whose family ``replace()`` rewrote. ``Reading.second_surname`` marks its words; it is not compared. :issue:`754` diff --git a/nameparser/config/_index.py b/nameparser/config/_index.py index b4a4b3026..6e48982b2 100644 --- a/nameparser/config/_index.py +++ b/nameparser/config/_index.py @@ -1061,9 +1061,9 @@ ('mga', (('cs', 'academic', (('cased', 'MgA'), ('default', False))),)), ('mgr', (('cs', 'academic', (('default', False),)), ('en', 'religious', ()), ('fr', 'religious', ()), ('sk', 'academic', (('default', False),)))), ('mgysgt', (('en', 'military', ()),)), - ('mhic', (('ga', 'clan', (('default', False),)),)), + ('mhic', (('ga', 'clan', (('cased', 'Mhic'), ('default', False))),)), ('mhsc', (('en', 'credential', (('cased', 'MHSc'),)),)), - ('mic', (('ga', 'clan', (('default', False),)), ('ga', 'given', ()), ('ga', 'family', ()))), + ('mic', (('ga', 'clan', (('cased', 'Mic'), ('default', False))), ('ga', 'given', ()), ('ga', 'family', ()))), ('micp', (('en', 'credential', ()),)), ('mieee', (('en', 'credential', ()),)), ('military', (('en', 'modifier', ()),)), @@ -1160,7 +1160,7 @@ ('neto', (('pt', 'agnomen', ()), ('pt', 'given', ()), ('pt', 'family', ()))), ('netto', (('pt-BR', 'agnomen', ()), ('pt-BR', 'family', ()))), ('neuroscientist', (('en', 'occupation', ()),)), - ('nic', (('ga', 'clan', (('default', False),)), ('ga', 'given', ()), ('ga', 'family', ()), ('gd', 'clan', (('default', False),)))), + ('nic', (('ga', 'clan', (('cased', 'Nic'), ('default', False))), ('ga', 'given', ()), ('ga', 'family', ()), ('gd', 'clan', (('cased', 'Nic'), ('default', False))))), ('nmd', (('en', 'credential', ()),)), ('novelist', (('en', 'occupation', ()),)), ('np', (('en', 'credential', ()),)), diff --git a/nameparser/locales/ga.py b/nameparser/locales/ga.py index 1b3da12ec..93a6e1624 100644 --- a/nameparser/locales/ga.py +++ b/nameparser/locales/ga.py @@ -6,7 +6,8 @@ Each is then a particle that joins the surname behind it: ``Máire Uí Bhriain`` gives family ``Uí Bhriain`` where the default gives middle ``Uí``. ``nic`` and ``mic`` are given names too, so a -leading one stays the given name, reported (rules/P1). +leading one stays the given name in a name read given-first, reported +(rules/P1), as ``Mac`` does by default. Data sources: the counts on each reading in ga.toml (tools/name_usage.py, ORCID's family box by country among them). The pack carries no lexicon diff --git a/nameparser/locales/gd.py b/nameparser/locales/gd.py index cdd8a7194..ffb74c436 100644 --- a/nameparser/locales/gd.py +++ b/nameparser/locales/gd.py @@ -8,8 +8,8 @@ Irish pack turns on, is left out: in Scottish Gaelic it mostly links a patronymic chain, which a second particle would split (gd.toml's note). ``Nic`` is a given name too (its reading is in ga.toml, and holds in -every choice), so a leading Nic stays the given name, reported -(rules/P1). +every choice), so a leading Nic stays the given name in a name read +given-first, reported (rules/P1), as ``Mac`` does by default. Data sources: the counts on each reading in ga.toml and gd.toml (tools/name_usage.py). The pack carries no lexicon fragment and no diff --git a/nameparser/vocab/AGENTS.md b/nameparser/vocab/AGENTS.md index c044bc5d5..409f512f3 100644 --- a/nameparser/vocab/AGENTS.md +++ b/nameparser/vocab/AGENTS.md @@ -22,6 +22,10 @@ Check the predicates that test a member's SPELLING rather than its membership: t Record what the vetting found ON the word, not in a comment or a decisions sentence: `note`, `issue`, a `given`/`family` reading with dated counts, and for a word you decided to keep out an `excluded` entry with its reason, so the next contributor who adds it meets the reason as a build error (decisions/vocabulary-model) — unless the reading is right in its own language and wrong only as a default, because another language collides with it, which is a reading written `default = false` instead, for a language choice to turn on (3.0's phase 3c, cs `ml`). A name reading is not only provenance on a word that has a credential, particle or title reading: it IS the clash, and derives the marker (an ambiguous acronym, a particle that may be given, since 2026-10-25, a generation or agnomen that may be a name, rules/S11, since phase 2h the same day, so given-name counts recorded on `junior` would gate it, and a title that may be a given name, rules/H9, since phase 2i), so counts kept as evidence AGAINST marking such a word go in the clashing reading's `note` instead, as `ld`'s do. +## A word that becomes a particle: its case + +A particle reading is lowercased by case repair (rules/R4) unless its word carries a `cased` mask, so a word the default capitalizes as a name word loses its capital the moment a reading makes it a particle, in the default or under the pack that turns it on. #956 turned on Irish `nic`, `mhic` and `mic` with no mask, and `MÁIRE NIC GIOLLA PHÁDRAIG` repaired to `Máire nic Giolla Phádraig` under `ga`, where the default gives `Nic`; `ní` and `uí` had their masks and kept it. Give a capitalized particle its `cased`, and test `capitalized()` both ways (repair and `force=True`) under the parser that reads it. + ## Step 6: the user docs' doctests The user docs use real words as examples of an UNLISTED one, and listing the word breaks the example: #797 listed `dphil`, and docs/customize.rst's capitalization example (`parse("jane smith dphil").capitalized()` giving `Dphil`) failed in all five CI build jobs while `uv run pytest` passed, because `sphinx-build -b doctest docs` is a separate step. Run it after adding a word, and grep `docs/*.rst` for the word; the example now uses `dclinpsy`, a credential left unlisted on purpose, so list it only after moving the example. diff --git a/nameparser/vocab/ga.toml b/nameparser/vocab/ga.toml index 1092c22cf..01918eced 100644 --- a/nameparser/vocab/ga.toml +++ b/nameparser/vocab/ga.toml @@ -28,7 +28,8 @@ may_be_given = false issue = 753 expands = "of the son of, the genitive of 'mac' after a woman's 'bean' or name: \"Máire Mhic Gabhann\", the married form of Mac Gabhann" same_as = "mac" -note = "Off by default with the rest of the Irish married and female forms: a particle the default has never read, turned on by an Irish pack (gd.toml says why Scottish Gaelic leaves it out). No source counts it as a given name (2026-10-30: ORCID 2 family boxes, both Ireland; PSC 1 filed name; none as a given name)." +cased = "Mhic" +note = "Off by default with the rest of the Irish married and female forms: a particle the default has never read, turned on by an Irish pack (gd.toml says why Scottish Gaelic leaves it out). No source counts it as a given name (2026-10-30: ORCID 2 family boxes, both Ireland; PSC 1 filed name; none as a given name). The mask is the word's own title case, as on 'ní': Irish writes it capitalized, and without one case repair would lowercase it as a particle (rules/R4)." [[word]] text = "mic" @@ -39,7 +40,8 @@ default = false issue = 753 expands = "of the son of, the unlenited genitive of 'mac'" same_as = "mac" -note = "Off by default: Mic is also a given name, a short form of Michael (its given reading), and a Romanian surname. An Irish pack turns it on, its given reading keeping a leading Mic a given name there, reported (rules/P1)." +cased = "Mic" +note = "Off by default: Mic is also a given name, a short form of Michael (its given reading), and a Romanian surname. An Irish pack turns it on, its given reading keeping a leading Mic a given name there, reported (rules/P1). The mask is the word's own title case, as on 'ní': Irish writes it capitalized, and without one case repair would lowercase it as a particle (rules/R4)." [[word]] text = "nic" @@ -50,7 +52,8 @@ default = false issue = [604, 753] expands = "daughter of, the female form beside 'mac': \"Máire Nic Gabhann\"" gender = "f" -note = "Off by default: Nic is also a given name, the short form of Nicholas (its given reading), and the default serves every language written in Latin script, where an English Nic is far likelier than an Irish particle (#604). An Irish or Scottish Gaelic pack turns it on, and its given reading keeps a leading Nic a given name there, reported (rules/P1)." +cased = "Nic" +note = "Off by default: Nic is also a given name, the short form of Nicholas (its given reading), and the default serves every language written in Latin script, where an English Nic is far likelier than an Irish particle (#604). An Irish or Scottish Gaelic pack turns it on, and its given reading keeps a leading Nic a given name there, reported (rules/P1). The mask is the word's own title case, as on 'ní': Irish writes it capitalized, and without one case repair would lowercase it as a particle (rules/R4)." [[word]] text = "ní" diff --git a/nameparser/vocab/gd.toml b/nameparser/vocab/gd.toml index 444fe58ac..585613fe7 100644 --- a/nameparser/vocab/gd.toml +++ b/nameparser/vocab/gd.toml @@ -15,5 +15,6 @@ default = false issue = [604, 753] expands = "daughter of, the female form beside 'mac', written spaced: \"Màiri Nic Leòid\" (glued, \"NicLeòid\" is one word and needs no particle)" gender = "f" -note = "Off by default, as in ga.toml, which holds the word's given and family readings: a Scottish Gaelic pack turns it on, and the given reading keeps a leading Nic a given name, reported (rules/P1). The genitive 'mhic' is left out of this file though ga.toml turns it on: written in Scottish Gaelic it mostly links a patronymic chain (\"Iain mac Dhòmhnaill mhic Sheumais\"), which a second listed particle splits at its last link, as the default already splits 'Iain mac Dhòmhnaill mac Sheumais' (2026-10-30)." +cased = "Nic" +note = "Off by default, as in ga.toml, which holds the word's given and family readings: a Scottish Gaelic pack turns it on, and the given reading keeps a leading Nic a given name, reported (rules/P1). The genitive 'mhic' is left out of this file though ga.toml turns it on: written in Scottish Gaelic it mostly links a patronymic chain (\"Iain mac Dhòmhnaill mhic Sheumais\"), which a second listed particle splits at its last link, as the default already splits 'Iain mac Dhòmhnaill mac Sheumais' (2026-10-30). The mask is the word's own title case, as on ga.toml's 'ní': Gaelic writes it capitalized, and without one case repair would lowercase it as a particle (rules/R4)." diff --git a/nameparser/vocab/pt-BR.toml b/nameparser/vocab/pt-BR.toml index 78f9c7936..462b6bd7f 100644 --- a/nameparser/vocab/pt-BR.toml +++ b/nameparser/vocab/pt-BR.toml @@ -44,7 +44,7 @@ family = { note = "no source counts it as a name; declared as neto's is, so that [[word]] text = "sb" -agnomen = { default = false, same_as = "sobrinho", gender = "m", issue = [843, 752, 753], expands = "the nephew, abbreviated", cased = "SB", note = "Off by default: tse-donors (2026-10-25) has 11 filed/registry pairs writing SB where the registry writes SOBRINHO, but SB is also S.B., the Bachelor of Science, which 23 ClinicalTrials.gov officials (9 after a comma) and 2 NPPES providers write (2026-10-27), and listed by default it read the degree as Sobrinho's spelling. A Brazilian Portuguese pack turns it on: it then moved 13 tse-donors names that the choice of Portuguese alone does not, and the registry spells SOBRINHO in every one (2026-10-30). The mask keeps the degree's capitals, which capitalized(force=True) recased to Sb when the word was listed by default" } +agnomen = { default = false, same_as = "sobrinho", gender = "m", issue = [843, 752, 753], expands = "the nephew, abbreviated", cased = "SB", note = "Off by default: tse-donors (2026-10-25) has 11 filed/registry pairs writing SB where the registry writes SOBRINHO, but SB is also S.B., the Bachelor of Science, which 23 ClinicalTrials.gov officials (9 after a comma) and 2 NPPES providers write (2026-10-27), and listed by default it read the degree as Sobrinho's spelling. A Brazilian Portuguese pack turns it on: it then moved 13 tse-donors names that the choice of Portuguese alone does not, and the registry spells SOBRINHO in every one (2026-10-30). The mask, the word's, recases Sobrinho's spelling too (José Silva Sb. force-repairs to SB.), and keeps the degree's capitals, which capitalized(force=True) recased to Sb when the word was listed by default" } family = { note = "no source counts it as a name; declared as sobrinho's is, so that two words ending in it keep it as the family name" } [[word]] diff --git a/tests/v2/rules_budget.json b/tests/v2/rules_budget.json index d32bc9761..63fd47b32 100644 --- a/tests/v2/rules_budget.json +++ b/tests/v2/rules_budget.json @@ -70,7 +70,7 @@ "S8": {"words": 571, "then_words": 406, "because_sentences": 1, "except": 1, "precedence": 2, "accepted": 1, "accepted_words": 63}, "S9": {"words": 463, "then_words": 262, "because_sentences": 2, "except": 0, "precedence": 3, "accepted": 0, "accepted_words": 0}, "S10": {"words": 665, "then_words": 246, "because_sentences": 3, "except": 4, "precedence": 3, "accepted": 3, "accepted_words": 163}, - "S11": {"words": 531, "then_words": 318, "because_sentences": 3, "except": 1, "precedence": 1, "accepted": 2, "accepted_words": 107}, + "S11": {"words": 529, "then_words": 316, "because_sentences": 3, "except": 1, "precedence": 1, "accepted": 2, "accepted_words": 107}, "S12": {"words": 674, "then_words": 372, "because_sentences": 2, "except": 3, "precedence": 4, "accepted": 2, "accepted_words": 93}, "S13": {"words": 431, "then_words": 180, "because_sentences": 3, "except": 2, "precedence": 1, "accepted": 4, "accepted_words": 138}, "T1": {"words": 93, "then_words": 18, "because_sentences": 1, "except": 1, "precedence": 0, "accepted": 3, "accepted_words": 83}, diff --git a/tests/v2/test_locales.py b/tests/v2/test_locales.py index 3e9fe3eeb..711a338ff 100644 --- a/tests/v2/test_locales.py +++ b/tests/v2/test_locales.py @@ -1729,6 +1729,25 @@ def test_the_3h_packs_under_a_family_first_order( (by_pack.family, by_pack.suffix)) == (default, packed) +@pytest.mark.parametrize("force", [False, True]) +@pytest.mark.parametrize("code, text, repaired", [ + ("GA", "MÁIRE NIC GIOLLA PHÁDRAIG", "Máire Nic Giolla Phádraig"), + ("GD", "MÁIRE NIC GIOLLA PHÁDRAIG", "Máire Nic Giolla Phádraig"), + ("GA", "MÁIRE MHIC GABHANN", "Máire Mhic Gabhann"), + ("GA", "SEÁN MIC GABHANN", "Seán Mic Gabhann"), + ("GA", "MÁIRE UÍ BHRIAIN", "Máire Uí Bhriain"), + ("GA", "máire nic gabhann", "Máire Nic Gabhann"), +]) +def test_the_irish_particles_keep_their_capital_under_their_pack( + code: str, text: str, repaired: str, force: bool) -> None: + # Irish writes these capitalized, as the default's repair does; a + # particle reading without a mask is lowercased by case repair + # (rules/R4), so each turned-on reading carries its own `cased` + packed = parser_for(locales.get(code)) + assert str(parse(text).capitalized(force=force)) == repaired + assert str(packed.capitalized(packed.parse(text), force=force)) == repaired + + def test_the_pt_br_pack_selects_pt_too() -> None: # pt-BR selects its ancestor's file (decisions/vocabulary-model's 3b # entry, item 6): Portuguese `e` is a connective in a one-case name From af9251272fd2f17ff6e68b2a2cf0cf5194366366 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Fri, 30 Oct 2026 03:41:05 -0700 Subject: [PATCH 4/5] Scope S11's default reading of SB and state the dotted Sr. reading Co-Authored-By: Claude Opus 5.5 --- docs/design/rules/S11-suffix-word-that-is-also-a-name.md | 2 +- docs/locales.rst | 3 ++- tests/v2/rules_budget.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/design/rules/S11-suffix-word-that-is-also-a-name.md b/docs/design/rules/S11-suffix-word-that-is-also-a-name.md index 947d896fa..a8d9dc5ac 100644 --- a/docs/design/rules/S11-suffix-word-that-is-also-a-name.md +++ b/docs/design/rules/S11-suffix-word-that-is-also-a-name.md @@ -4,7 +4,7 @@ **Then:** it reads as the generation only where the name has words to spare, as a bare ambiguous acronym does (S4), and at the slots that report, either reading carries the ambiguity flag. -- **The writing is read by S6**, and for a generation word such as French `fils` it speaks at the other end of the case: lower case in a name of mixed case is the generation, Title case leans to the name, capitals say nothing. The floor holds first: lower case settles it only where the name keeps a family word, so two name words keep the family, and at the first slot after a comma the count of name words before it decides alone (C4). An agnomen, the Portuguese `Filho`, `Neto`, `Sobrinho` and their feminines, is written in the case of the name around it, so its writing never speaks and the count decides alone; so is each abbreviation of one that Brazilian filings write (`FH`, `Fo`, `Nt`, `Sobr`), a spelling of the word it shortens that takes the word's name reading with it, borne or not. Under a Brazilian Portuguese choice so is `SB`, which by default reads as a name word (the family). +- **The writing is read by S6**, and for a generation word such as French `fils` it speaks at the other end of the case: lower case in a name of mixed case is the generation, Title case leans to the name, capitals say nothing. The floor holds first: lower case settles it only where the name keeps a family word, so two name words keep the family, and at the first slot after a comma the count of name words before it decides alone (C4). An agnomen, the Portuguese `Filho`, `Neto`, `Sobrinho` and their feminines, is written in the case of the name around it, so its writing never speaks and the count decides alone; so is each abbreviation of one that Brazilian filings write (`FH`, `Fo`, `Nt`, `Sobr`), a spelling of the word it shortens that takes the word's name reading with it, borne or not. Under a Brazilian Portuguese choice so is `SB`, which by default ends a name as its family word, or behind a credential as a suffix by its shape. - **Only name words in front of it count**: a word there that the trailing run takes, a suffix word, a roman numeral read as the generation (S10), a member of the ambiguous class its writing makes a suffix (S6) or one such a word carries in front of it (S7), is no word to spare, and a suffix word there is no company either, as it is for an acronym (S7). - **After a comma** the word is read as a member of the ambiguous class is: by the count of the name words before the comma at the first slot after it (C4), and by its writing alone where it ends the given part after a family comma (S5). diff --git a/docs/locales.rst b/docs/locales.rst index 726d04852..67a09d6fa 100644 --- a/docs/locales.rst +++ b/docs/locales.rst @@ -512,7 +512,8 @@ readings of those words: - **Under pt_br,** the words read as ``pt`` reads them: ``John Smith DO`` gives family ``DO``, and a trailing ``Sr`` is read as the Portuguese title *Senhor* rather than English "Senior", so ``John Smith Sr`` - gives family ``Sr``, a known cost not yet settled. + gives family ``Sr`` and the dotted ``John Smith Sr.`` title ``Sr.``, + a known cost not yet settled. - **Under pt_br,** a written S.B. degree reads as Sobrinho (``Howard Gordon, MD SB`` fills ``generation`` with ``SB``). diff --git a/tests/v2/rules_budget.json b/tests/v2/rules_budget.json index 63fd47b32..faae8da7f 100644 --- a/tests/v2/rules_budget.json +++ b/tests/v2/rules_budget.json @@ -70,7 +70,7 @@ "S8": {"words": 571, "then_words": 406, "because_sentences": 1, "except": 1, "precedence": 2, "accepted": 1, "accepted_words": 63}, "S9": {"words": 463, "then_words": 262, "because_sentences": 2, "except": 0, "precedence": 3, "accepted": 0, "accepted_words": 0}, "S10": {"words": 665, "then_words": 246, "because_sentences": 3, "except": 4, "precedence": 3, "accepted": 3, "accepted_words": 163}, - "S11": {"words": 529, "then_words": 316, "because_sentences": 3, "except": 1, "precedence": 1, "accepted": 2, "accepted_words": 107}, + "S11": {"words": 539, "then_words": 326, "because_sentences": 3, "except": 1, "precedence": 1, "accepted": 2, "accepted_words": 107}, "S12": {"words": 674, "then_words": 372, "because_sentences": 2, "except": 3, "precedence": 4, "accepted": 2, "accepted_words": 93}, "S13": {"words": 431, "then_words": 180, "because_sentences": 3, "except": 2, "precedence": 1, "accepted": 4, "accepted_words": 138}, "T1": {"words": 93, "then_words": 18, "because_sentences": 1, "except": 1, "precedence": 0, "accepted": 3, "accepted_words": 83}, From f7f1b6323ebdec168064982c2dd98e3e5a17756e Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Fri, 30 Oct 2026 12:35:58 -0700 Subject: [PATCH 5/5] Hold every word the 3h choices change in their rotators Master's test_choice_rotators_cover_every_changed_word (3d) asks a rotator for each word of a choice's derived surface, which takes in the words a choice prefers as well as those it turns on: mc under ga and gd, and pt's do, e and sr under pt_br. Co-Authored-By: Claude Opus 5.5 --- tests/v2/test_locales.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/v2/test_locales.py b/tests/v2/test_locales.py index 711a338ff..d8e768321 100644 --- a/tests/v2/test_locales.py +++ b/tests/v2/test_locales.py @@ -1074,24 +1074,30 @@ def test_choice_rotators_cover_every_changed_word(code: str) -> None: # The vocabulary packs' rotators: a name in each shape a reading the pack -# turns on changes, read one way by default and another under the pack -# (the pinned rows below say how). ga: each clan particle mid-name; -# gd: `nic`, its one word; pt_br: `sb` behind name words, bare and -# dotted, in each case. +# turns on or prefers changes, read one way by default and another under +# the pack (the pinned rows below say how). ga: each clan particle +# mid-name; gd: `nic`; both: the clan `mc` preferred over the credential; +# pt_br: `sb` behind name words, bare and dotted, in each case, and the +# words pt's choice prefers. _ROTATORS["ga"] = [ "Máire Nic Gabhann", # nic "Máire Mhic Gabhann", # mhic "Máire Uí Bhriain", # uí "Pádraig Mic Cárthaigh", # mic + "John Smith MC", # mc, the clan particle, not MC ] _ROTATORS["gd"] = [ "Màiri Nic Leòid", # nic "Gabhann, Màiri Nic", # nic ending the given part + "John Smith MC", # mc, the clan particle, not MC ] _ROTATORS["pt_br"] = [ "JOSE CARLOS SILVA SB", # sb "Jose Carlos Silva Sb.", # sb, dotted "JOSE SILVA SB", # sb, one word to spare + "John Smith DO", # do, pt's particle, not DO + "ANA SILVA E SOUZA", # e, pt's connective + "Gary Holloway Sr", # sr, pt's Senhor, not the generation ]