From b1a685b6f76030528d66c36421770006ddb18027 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sun, 25 Oct 2026 17:29:52 -0700 Subject: [PATCH 1/6] Settle a listed credential written in capitals (#752, phase 2g) In a name written in more than one case, a listed member of the ambiguous credential class written in capitals (MA, DO, BA) was already read as the credential, but it was still reported as a suffix-or-name fork, and it did not start S8's credential run where its dotted spelling did. The capitals are the writing's own answer, as the periods are, so the clash is now settled at the gate's form step: the report goes (#785's first question), the member starts the run (its second), and a name written in one case keeps its report (its third). The word is not retagged. The phase 2 spike that retagged it erased its other readings (`Doe, John DO` became family 'DO Doe' through P6, and `Jane Doe nee MA` lost its maiden name), so one predicate, `_pieces.settled_by_case`, is asked where a reading is chosen: the trailing peel takes a settled member without recording a pick, the comma's decision and the given part's trailing slot leave its report out, and the run start admits it, its particle test still refusing DO. Accepted: a writer's initials in capitals between the given names and the family name, where they spell a listed member, start the run (`Maria Ana MA Silva` -> suffix 'MA Silva'), as the dotted spelling already did; 6 ctgov names take that shape. Rules S6, S8, S5, S4, S3, S7, C11 and M2 amended; dated entries in decisions/S6 (the record), S8, S5, C11, S3 and vocabulary-model; case rows for the new readings, and rows that pinned an emitter rewritten in one case or lower case so they still reach it. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- docs/concepts.rst | 6 +- docs/customize.rst | 5 +- .../C11-what-the-comma-decision-reports.md | 3 +- ...S3-credentials-written-in-joined-chunks.md | 3 +- ...arts-trailing-slot-after-a-family-comma.md | 5 +- .../S6-case-signal-at-the-suffix-slot.md | 9 + .../decisions/S8-credentials-trailing-run.md | 7 +- docs/design/decisions/vocabulary-model.md | 5 +- .../C11-what-the-comma-decision-reports.md | 10 +- docs/design/rules/M2-maiden-marker-rule.md | 7 +- ...S3-credentials-written-in-joined-chunks.md | 11 +- ...-ambiguous-acronym-needs-words-to-spare.md | 2 +- ...arts-trailing-slot-after-a-family-comma.md | 17 +- .../S6-case-signal-at-the-suffix-slot.md | 25 +- ...S7-company-of-an-unambiguous-credential.md | 2 +- .../rules/S8-credentials-trailing-run.md | 19 +- docs/design/vocabulary-model-rfc.md | 2 +- docs/release_log.rst | 1 + docs/usage.rst | 12 +- nameparser/_pipeline/_assign.py | 25 +- nameparser/_pipeline/_comma.py | 19 +- nameparser/_pipeline/_pieces.py | 39 +- tests/v2/cases.py | 626 +++++++++--------- tests/v2/pipeline/test_assign.py | 62 +- tests/v2/pipeline/test_comma.py | 7 +- tests/v2/pipeline/test_group.py | 4 +- tests/v2/pipeline/test_pieces.py | 27 +- tests/v2/pipeline/test_post_rules.py | 8 +- tests/v2/rules_budget.json | 14 +- tests/v2/test_cases.py | 12 +- tests/v2/test_contracts.py | 4 +- tests/v2/test_parser.py | 22 +- tests/v2/test_properties.py | 49 +- tools/differential/corpus_rules.jsonl | 19 +- tools/differential/corpus_shapes.jsonl | 11 +- tools/differential/snapshot.jsonl | 129 ++-- 37 files changed, 771 insertions(+), 459 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 54cbb98c6..90563feb2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -186,7 +186,7 @@ The library has two layers: `nameparser/config/` (data) and `nameparser/parser.p **Sweep the forms a change can reach: comma shapes, then `name_order`.** No comma, a FULL name before the comma, a ONE-WORD name before the comma and NOTHING before the comma are four paths, and the last two are the misses — #429, #430 and #432 are all the one-word path disagreeing with the full-name path on inputs the full-name path reads correctly, and #613's PR review found `, PhD` and `, Jr.` (an empty surname field, as CSV data writes it) read as given and title by a decision that returned early on an empty head, after two review rounds whose every grid had a name before the comma. Put an empty head and a nickname-only head (`(Bob), PhD`) in every comma grid. Count the one-word path the way C3 counts, which is neither tokens nor pieces: a particle run and the one word it attaches to are one word (#575), a connective join and a bound given-name pair count as their words (#613, so `Ortega y Gasset, Dr.` is a whole name), and a suffix beside the surname counts as no name word at all. #603's review found the suffix half (`Vega y Lopez Jr., PhD Jones` lost its family where `Vega y Lopez, PhD Jones` kept it); #613 then reversed the connective half, and both now read given 'Vega', middle 'y', family 'Lopez' — so test a particle surname, a connective surname and a surname with a suffix, each before a comma. For orders the sweep already exists (`tests/v2/test_cases.py` runs every row under all three) but carries one assertion, R2's family partition, so it checks nothing a new change moves; coverage there has been vacuous before (PR #394's review found the suite passed with `name_order` discarded from grouping). Parse your change's names in each comma shape and each order, and read the ones you did not predict. And read a generated grid's moves by SHAPE, not by corpus membership: #614's first design passed the suite, the five gates and a fingerprint of every corpus name, because its grid moves were sorted into 'corpus name moved' and 'garbage', and the review found the moves that mattered among the 'garbage' -- a bare salutation (`Mr. and Mrs.` lost its connective to the family name) and a name with one trailing word (`Freiherr von Berg X.Y.Z.`). Pull out the head-plus-one-word slice of the grid and a salutation with nothing after it, and read every move there. Two more were the second review's (#614): no grid put TWO words that are both a title and a particle at the head (`Freiherr St van Berg MA`, where an index into the pieces was applied to a merged view of them -- `TITLES ∩ particles_ambiguous` is `{freiherr, st}`, so put both in every head list, each in its bare and its period-marked spelling -- #637's grid had `St` and `Freiherr` alone and missed that `St.` and `Freiherr.` are declined by the bound join and then attached by P6), and the grids compared ambiguity KINDS, so a report telling the caller the wrong field (`Freiherr von Berg Ma`, 'read as a given name' of a word in the family) passed every pin. #701's review added one: put a title/suffix dual (`Ms`, `Sr.`, `Cpl`) SECOND in a title run, behind an ordinary title. A stop that asks only "is this word suffix vocabulary" passes every row that opens with the dual and fails `Rev. Sr. Smith, Mary`; it was found by comparing `T Smith, John` against `T John Smith` over every title word, and a comma-versus-comma-less comparison is the cheap check for any rule that moves a word between the two slots. Compare each report's `detail` against the field its tokens land in -- since #626 `test_a_report_names_the_field_its_word_lands_in` (tests/v2/test_cases.py) does it over every non-locale case row under all three orders (a row carrying its own policy only as declared), for the phrasings its `_FIELD_CLAIMS` lists -- so a row with the shape is the test only where the report's wording is listed: a new emitter's wording goes in that list, and segment's report on a part past the second comma names no field (#629). #626's own first grid then missed an emitter for want of one comma shape (`, Ma Dr.`: an empty head, an ambiguous word, a trailing title, which H1 moves to the family): a trailing title belongs in every empty-head comma grid too. And #620's: an exclusion list in code being replaced is a set of rules, not noise -- #614's merged copy excluded every word the trailing read weighs from its units, the first #620 prototype dropped the list as a symptom of the copy, and its review found the list had been keeping positional counting's rule that a member in front counts as a word (`Freiherr von Berg MA X.Y.Z.` lost its suffix). Before deleting one, mutate each entry out and read what moves. The converse holds for replacing a hand copy WITH the shared predicate: it is a behavior change wherever the two disagreed, and #735's /simplify shipped one as "no reading moves" because the suite and the snapshot were its only check (`St Sir de Mesnil` family-first, a token test replaced by P1's piece test); diff the two predicates' answers over a grid before calling the swap a refactor. -**Then sweep the spellings: a class reached by vocabulary is also reached by SHAPE.** A test that uses only the listed spelling of a word class walks only the vocabulary path, and the shape path is a separate branch that can be missing while every test passes. Roman numerals are the sharpest case: `ii`, `iii` and `iv` are suffix vocabulary; `i` and `v` are vocabulary too but initial-shaped, so `is_suffix_piece` vetoes them and only the numeral fork reads them; and `vi` through `x` are in no list at all, read by the fork's shape test alone (checked 2026-10-04 against `Lexicon.default()`) -- which stops at `x`: `_vocab._ROMAN` matches I to X, so `John Smith XI` reads `XI` as a name word (re-checked 2026-10-07, #614's /simplify, which found a docstring claiming "any length"). And the fork counts a ONE-letter numeral besides, since #718 (rules/S10): it is the suffix only where the name has a word to spare, so `John V` and `John Smith V` take different branches of one fork, a one-case name being counted like capitals whatever the letter -- since #721 a one-case one-letter numeral carries the `initial` tag its capital does, and since #723 every one-case bare letter does (rules/T4), and a bare letter in front of the numeral is initial-shaped as its capital is, so `john v` reads as `JOHN V` does over `test_a_one_case_name_reads_as_its_capitals`'s grid (comma shapes, orders, a maiden clause, a particle chain, a bare letter in front, a trailing title, a non-ASCII letter in front: 315 of its then 1,872 pairs disagreed at 2bfa9533, before #721, and none after). Its first version claimed "every shape" with no bare letter in front and no trailing title in the grid, and #721's reviews found both broken; a grid's claim is the grid -- put a head-plus-numeral row beside every words-to-spare row. So a change that handles `V` and `III` can still miss `VI` — #610 found exactly that, the maiden take's candidate loop admitting a shape-only numeral in one position where the peel reads it in two (`Jane Doe nee Smith VI Prof.` kept maiden `Smith VI` while `V` and `III` worked). #656's sweep found the same class from the other side: C12 takes the word ending the name before a comma as EVIDENCE, and its first draft asked the suffix-word tag, so `John Smith VI, AD` missed the rule `III` reached, and then counted the whole part, so `Smith VI, AD` fired with `VI` as its own name word and `Smith V, AD` with a `V` that S10 reads as the family. A rule that weighs a numeral as a generation must ask the predicate that READS it as one (the trailing fork, `_vocab.reads_as_generation`, and its words-to-spare count), not the vocabulary that lists some of its spellings. The same split runs through the credentials (`MA` listed, `X.Y.Z.` by the dotted shape, `XYZ` by the caps shape, `MD/PhD` by its listed chunks and `PU-PH` by the caps shape's joined spelling, #752, each with its joiner glued, spaced and glued to one side, which are three token streams -- and a new chunk reading inherits the old one's exceptions, which #752's first draft did not (`Jack I-V` lost its family to the listed numerals, and the fix's length test counted a chunk's period, so `Jack I.-V.` did too, found by the second review), and `Ph. D.` merged by group into one flagged piece the peel's walk never holds -- #603 found #602's run-start predicate refusing it too, so `Smith, John Ph. D. Jones` kept middle 'Jones' where `Smith, John PhD Jones` read suffix 'PhD Jones') and the titles (`Prof.` period-marked and read at the end of a name, `Prof` bare and a name word there, and `Mr. and Mrs.` a run P3 joins, carrying a piece tag a one-word vocabulary test never sees -- PR #735's review found C12's hand copy of C13's title test (C8's until its 2026-10-19 split) reading `John Smith Jr., Mr. and Mrs. AD` as all suffix; ask C13's predicates in C13's order, `not is_suffix_piece` then `is_title_piece`, not the token tags, since `is_title_piece` alone reads the duals `Ms` and `Sr.` as titles). When a change touches a word class, test one spelling from each path. The same holds for a separator's SPACING: a comma (or any punctuation) can stand alone, glue to the word before it or glue to the word after, and each is a different token stream. #653's first fix swept `(née, Jones)` and missed `(née , Jones)`, where the kept comma became a word after the marker and M3 read the clause as maiden ', Jones'; its review found it. Put all three spacings in any grid around a separator. And a separator glued to a word hides the word from every vocabulary test, not only the one being changed: the semicolon delimiter's first draft read a glued word's stem in C1's run test alone and put 365 of ClinicalTrials.gov's reversed names right, missing the ones C13, C4 and C6 read off a word's TAGS (`MBBS;`, `MA;`); tagging the stem put 412 right, and reading it again in the comma decision's shape tests, which read a word's text rather than its tags (`MBBS;`), 431 (decisions/C10, 2026-10-24). A rule over a glued spelling reads the stem everywhere the word is read, its tags and every test of its text, and is swept against the space twin -- with a declared word delimiter too, which a substring test finds inside other words ('Stand' under ' and ', the review of the same change). A full stop INSIDE a word has four spellings of its own: initials run together (`J.R.R.`), joined words each closed by a stop (`Lt.Gov.`, `Msc.Ed.`), a joined word or name closed by none (`St.John`, `J.Smith`), and an abbreviation's own ending, closed or not (Italian `Sig.ra`, Latvian `Dr.philol.`). Shape tells the last from none of the others, so a rule over the shape decides which it costs; #838 found the ending only through the snapshot's CLDR row `Dut.ra Prof.ra`, and its first narrowing traded `St.John` for `Sig.ra` while the snapshot listed the `Dr.philol.` move among moves read as intended, until the design-docs review named it. Initials run together are also how some SURNAMES are written (Nepali `B.K.`, `K.C.`), and a caseless script writes its abbreviations, degrees included, as dotted letters (Thai `พ.บ.`): #864 found S3's position reading taking the first from 2.4.0 on, visible only once #866's Nepal candidates gave the corpora a population that writes them, and its first fix moved CLDR's Thai degrees into the name. A rule over dotted letters reads both before it ships. A predicate over ADJACENT PAIRS of words meets two shapes no grid of one word holds: an overlapping pair and a joining word beside an initial. #752's split-credential test (phase 2e) read `Dr. Ph.` in `Jane Doe Dr. Ph. D.` as the listed `drph`, leaving `D.` a word of its own (found by the maiden take's candidate grid), and a particle beside a surname written short as the listed `ded` (`DE D JUNIOR`, found only in tse-donors' readings), so put a title, a particle and a connective beside each pair spelling, and a pair overlapping another, in the grid. +**Then sweep the spellings: a class reached by vocabulary is also reached by SHAPE.** A test that uses only the listed spelling of a word class walks only the vocabulary path, and the shape path is a separate branch that can be missing while every test passes. Roman numerals are the sharpest case: `ii`, `iii` and `iv` are suffix vocabulary; `i` and `v` are vocabulary too but initial-shaped, so `is_suffix_piece` vetoes them and only the numeral fork reads them; and `vi` through `x` are in no list at all, read by the fork's shape test alone (checked 2026-10-04 against `Lexicon.default()`) -- which stops at `x`: `_vocab._ROMAN` matches I to X, so `John Smith XI` reads `XI` as a name word (re-checked 2026-10-07, #614's /simplify, which found a docstring claiming "any length"). And the fork counts a ONE-letter numeral besides, since #718 (rules/S10): it is the suffix only where the name has a word to spare, so `John V` and `John Smith V` take different branches of one fork, a one-case name being counted like capitals whatever the letter -- since #721 a one-case one-letter numeral carries the `initial` tag its capital does, and since #723 every one-case bare letter does (rules/T4), and a bare letter in front of the numeral is initial-shaped as its capital is, so `john v` reads as `JOHN V` does over `test_a_one_case_name_reads_as_its_capitals`'s grid (comma shapes, orders, a maiden clause, a particle chain, a bare letter in front, a trailing title, a non-ASCII letter in front: 315 of its then 1,872 pairs disagreed at 2bfa9533, before #721, and none after). Its first version claimed "every shape" with no bare letter in front and no trailing title in the grid, and #721's reviews found both broken; a grid's claim is the grid -- put a head-plus-numeral row beside every words-to-spare row. So a change that handles `V` and `III` can still miss `VI` — #610 found exactly that, the maiden take's candidate loop admitting a shape-only numeral in one position where the peel reads it in two (`Jane Doe nee Smith VI Prof.` kept maiden `Smith VI` while `V` and `III` worked). #656's sweep found the same class from the other side: C12 takes the word ending the name before a comma as EVIDENCE, and its first draft asked the suffix-word tag, so `John Smith VI, AD` missed the rule `III` reached, and then counted the whole part, so `Smith VI, AD` fired with `VI` as its own name word and `Smith V, AD` with a `V` that S10 reads as the family. A rule that weighs a numeral as a generation must ask the predicate that READS it as one (the trailing fork, `_vocab.reads_as_generation`, and its words-to-spare count), not the vocabulary that lists some of its spellings. The same split runs through the credentials (`MA` listed, `X.Y.Z.` by the dotted shape, `XYZ` by the caps shape, `MD/PhD` by its listed chunks and `PU-PH` by the caps shape's joined spelling, #752, each with its joiner glued, spaced and glued to one side, which are three token streams -- and a new chunk reading inherits the old one's exceptions, which #752's first draft did not (`Jack I-V` lost its family to the listed numerals, and the fix's length test counted a chunk's period, so `Jack I.-V.` did too, found by the second review), and `Ph. D.` merged by group into one flagged piece the peel's walk never holds -- #603 found #602's run-start predicate refusing it too, so `Smith, John Ph. D. Jones` kept middle 'Jones' where `Smith, John PhD Jones` read suffix 'PhD Jones') and the titles (`Prof.` period-marked and read at the end of a name, `Prof` bare and a name word there, and `Mr. and Mrs.` a run P3 joins, carrying a piece tag a one-word vocabulary test never sees -- PR #735's review found C12's hand copy of C13's title test (C8's until its 2026-10-19 split) reading `John Smith Jr., Mr. and Mrs. AD` as all suffix; ask C13's predicates in C13's order, `not is_suffix_piece` then `is_title_piece`, not the token tags, since `is_title_piece` alone reads the duals `Ms` and `Sr.` as titles). When a change touches a word class, test one spelling from each path. The same holds for a separator's SPACING: a comma (or any punctuation) can stand alone, glue to the word before it or glue to the word after, and each is a different token stream. #653's first fix swept `(née, Jones)` and missed `(née , Jones)`, where the kept comma became a word after the marker and M3 read the clause as maiden ', Jones'; its review found it. Put all three spacings in any grid around a separator. And a separator glued to a word hides the word from every vocabulary test, not only the one being changed: the semicolon delimiter's first draft read a glued word's stem in C1's run test alone and put 365 of ClinicalTrials.gov's reversed names right, missing the ones C13, C4 and C6 read off a word's TAGS (`MBBS;`, `MA;`); tagging the stem put 412 right, and reading it again in the comma decision's shape tests, which read a word's text rather than its tags (`MBBS;`), 431 (decisions/C10, 2026-10-24). A rule over a glued spelling reads the stem everywhere the word is read, its tags and every test of its text, and is swept against the space twin -- with a declared word delimiter too, which a substring test finds inside other words ('Stand' under ' and ', the review of the same change). A full stop INSIDE a word has four spellings of its own: initials run together (`J.R.R.`), joined words each closed by a stop (`Lt.Gov.`, `Msc.Ed.`), a joined word or name closed by none (`St.John`, `J.Smith`), and an abbreviation's own ending, closed or not (Italian `Sig.ra`, Latvian `Dr.philol.`). Shape tells the last from none of the others, so a rule over the shape decides which it costs; #838 found the ending only through the snapshot's CLDR row `Dut.ra Prof.ra`, and its first narrowing traded `St.John` for `Sig.ra` while the snapshot listed the `Dr.philol.` move among moves read as intended, until the design-docs review named it. Initials run together are also how some SURNAMES are written (Nepali `B.K.`, `K.C.`), and a caseless script writes its abbreviations, degrees included, as dotted letters (Thai `พ.บ.`): #864 found S3's position reading taking the first from 2.4.0 on, visible only once #866's Nepal candidates gave the corpora a population that writes them, and its first fix moved CLDR's Thai degrees into the name. A rule over dotted letters reads both before it ships. Capitals between two name words have the same second reading: some writers put their own initials there (`Maria Ana MA Silva`), so a rule that reads a word's capitals as a credential reads the initials too; #785 met it only in ctgov's field moves, no grid having held the shape, and accepted it (rules/S8). A predicate over ADJACENT PAIRS of words meets two shapes no grid of one word holds: an overlapping pair and a joining word beside an initial. #752's split-credential test (phase 2e) read `Dr. Ph.` in `Jane Doe Dr. Ph. D.` as the listed `drph`, leaving `D.` a word of its own (found by the maiden take's candidate grid), and a particle beside a surname written short as the listed `ded` (`DE D JUNIOR`, found only in tse-donors' readings), so put a title, a particle and a connective beside each pair spelling, and a pair overlapping another, in the grid. **And the encoding: a letter class written `[^\W\d_]` or tested with `isalpha()` is blind to a combining mark.** Python's `\w` and `isalpha()` do not match Mn, so a letter written as base plus accent (NFD) ends a word early; the vocabulary path is immune because `_normalize` NFC-folds, the shape path is not. #652: `Dr. inž. Janez Novak` read `inž.` as the given name when the ž was decomposed, and `Janez Novak, ŽŠČ` reversed, in two families that share the blind spot — the letter-shape regexes (`period_abbreviation`, `_DOTTED`, `_CHUNKED`, `_PAIRED_INITIALS`), now spelled with `_vocab._LETTER` (a letter plus the general combining blocks; `test_regex_sync` compares the two `_MARKS` strings and pins `period_abbreviation` against config's copy), and the caps predicates, which NFC-compose a non-ASCII word before `isalpha()` (`is_caps_word`, behind C-level `isalpha()`/`is_normalized` tests, so already-composed text pays no frame — measured 2026-10-12, `Smith, José` was 193 frames before and after; the first version called it for every non-ASCII word and cost one frame each, and no guard saw it because the reference name is ASCII). A width test must count letters, not code points (`composed_len`): admitting a decomposed word to the caps set put `John Smith, MÁ` on the pair exception's wrong side, and the review's grid found it where the rows (all three letters long) did not — put a two-letter word in every caps grid. The initial shape (`_INITIAL`, `\w\.`, v1-verbatim and public) is not widened but asked of the COMPOSED word, in `is_initial_shaped` and in `_render`'s two copies; and the single-letter connective tests count composed letters, since a decomposed `й` is two code points (#665, where `É. y É. Smith` and `John й Smith` read differently decomposed). Compose the WHOLE predicate, not one half: `is_initial` is a shape ANDed with the #320 script veto, and composing the shape alone made a decomposed `씨.` an initial, since decomposed hangul is conjoining jamo outside every script range (#665's first version; `in_initialless_script` composes too now). Not covered, deliberately: the marks only some scripts write (Indic vowel signs, Arabic and Hebrew points), declined in decisions/nfd-shapes because widening the class would move readings nobody had measured; rules/H2's Accepted clause records the one consequence it names, that a period-marked abbreviation in Bengali or Devanagari never reads as a title by shape. The guard is `test_a_decomposed_name_renders_as_its_composed_twin` over the CLDR corpus plus `test_a_decomposed_abbreviation_reads_as_its_composed_twin` (rows × three orders × EVERYWHERE); the corpus guard reaches a shape only if some corpus name does, which is how the caps shape was missed until review, so when adding a shape rule add a row there. Four of #652's rows and two of #665's pass with every fix reverted (measured) and are parity pins, and the test compares ambiguity kinds and `initials()` too, since #665's `John й Smith` differed in its report; `_DOTTED` has no observable input and is pinned by a direct call. diff --git a/docs/concepts.rst b/docs/concepts.rst index 01ca5629d..cf73dd84b 100644 --- a/docs/concepts.rst +++ b/docs/concepts.rst @@ -213,8 +213,10 @@ ambiguous post-nominal vocabulary, but in ``"Joao da Silva do Amaral de Souza"`` it sits mid-name, where nothing has to choose between readings — so nothing is recorded. A comma can settle the question before it arises, too: ``"Ma, Jack"`` fixes the family name, so the -credential reading never comes up, while ``"John Smith MA"`` has to -call it and says so. Some decisions are conventions rather than +credential reading never comes up, and so can the writing: +``"John Smith M.A."`` and, in a mixed-case name, ``"John Smith MA"`` are +written as credentials, while ``"John Smith Ma"`` has to call it and +says so. Some decisions are conventions rather than readings — a name whose one name word nothing else decides (such as a title, a family comma, a nickname or maiden name beside it, a script that settles its own order, or the word's own vocabulary or diff --git a/docs/customize.rst b/docs/customize.rst index ca0c92e93..416491d29 100644 --- a/docs/customize.rst +++ b/docs/customize.rst @@ -335,7 +335,10 @@ capitalization decides: The full reading of a bare marked acronym, in order: - **ALL CAPITALS** in a mixed-case name leans to the credential: it is - a suffix even with no words to spare (``"Jack MA"``). + a suffix even with no words to spare (``"Jack MA"``). The capitals + settle it as the periods do, so no ambiguity is reported, and a word + after it reads as part of the suffix (``"John Smith MA Jones"`` gives + suffix ``MA Jones``). - **Title-case** in a mixed-case name leans to the surname: it stays the family name even WITH words to spare (``"John Smith Ma"``). - **No signal** — all lowercase in a mixed-case name, or any spelling diff --git a/docs/design/decisions/C11-what-the-comma-decision-reports.md b/docs/design/decisions/C11-what-the-comma-decision-reports.md index 00fe0424b..e26055d66 100644 --- a/docs/design/decisions/C11-what-the-comma-decision-reports.md +++ b/docs/design/decisions/C11-what-the-comma-decision-reports.md @@ -3,7 +3,7 @@ ## Standing - A part read wholly as suffixes carries no report that a word of it was read as a name, so a particle chained or an acronym taken into a name is never reported there: `John Smith, Jr., Freiherr von Richthofen` reports only `comma-structure` — 2026-09-28 #544. -- A word of the ambiguous class opening the part after the comma carries the comma's report even where its capitals settle the run (`Doe, MA PhD`, `John Smith, MA MA`, `Smith, MA PhD` each report `suffix-or-name` once), while a capitals-settled run behind another credential is silent (`John Smith, PhD MA`); no `suffix-or-name` report lands past the second comma (`Smith, John, MA`, `John Smith, Jr., MA`) — 2026-10-01. +- A word of the ambiguous class its capitals settle carries no report wherever it stands in the part after the comma (`Doe, MA PhD`, `John Smith, MA MA`, `Smith, MA PhD`, `John Smith, PhD MA`, `John Smith, MA` are silent), a word the capitals do not settle reporting as before (`Doe, Ma PhD`, `JOHN SMITH, MA`) — 2026-10-25 #785, superseding the 2026-10-01 report on an opening word; no `suffix-or-name` report lands past the second comma (`Smith, John, MA`, `John Smith, Jr., MA`) — 2026-10-01. - The part after a suffix comma reports each word a credential in front of it reads as the run although it may be a name word (`John Smith, PhD van der Berg` → `suffix-or-name` on 'van', 'der' and 'Berg'), and C2's no-name-reading clause means only a word read as a name — 2026-10-11 #641. Elsewhere: no particle chain reaches such a part at all, which replaced #544's withheld report list: the part after a suffix comma is bound before any join — decisions/C1's 2026-10-06 (Derek), #613 entry — and a part past the second comma makes no chain — decisions/C7's 2026-10-11 (Derek), #636 entry; `pair_only` asks `is_title_piece` — decisions/C5's 2026-10-19 (Derek), follow-up to PR #738 entry; the listed-member report names the role its word is bound to ("a title" for `John, Si.`, "a credential" for `John, MA`) — decisions/suffix-acronym-collisions' 2026-10-21 (#793) entry and decisions/A1's 2026-10-21 #793 entry; a maiden marker past the second comma being an ordinary word, so `Smith, John, Jr nee Jones MA` reads suffix 'Jr nee Jones MA' where the 2026-10-01 entry read a maiden clause — decisions/M2's 2026-10-04 (Derek), #601 entry. @@ -18,3 +18,4 @@ Open: none in this file. - 2026-10-18 #728 — SPLIT FROM decisions/C1 with the rule. The entries above were written under decisions/C1 while this rule was part of rules/C1, and moved here unchanged but for the pointers the move broke, the measurement paragraphs, each cut to one line naming its population, counts, baseline and PR, and the superseded passages, each cut to its decision and what replaced it; where one says rules/C1 or C1, it means the rule as it then stood, which held this one. - 2026-10-19 (Derek), follow-up to PR #738 — THE REPORT'S PAIRED-INITIALS EXCEPTION (`pair_only`) ASKS `is_title_piece` FOR THE TITLES IN FRONT OF THE FIRST PAIR. Recorded under decisions/C5 with the speaker scan it was changed beside, the measurement and the one configured move. - 2026-10-21 #793 — THE LISTED-MEMBER REPORT NAMES THE ROLE ITS WORD IS BOUND TO, "a title" where the listing form binds the member as one (`John, Si.`) and "a credential" where it binds a suffix (`John, MA`). Recorded under decisions/suffix-acronym-collisions with its grid and measurement, and as an amendment in decisions/A1. +- 2026-10-25 #785 (Derek, 2026-10-23) — A DECISION THE CAPITALS SETTLED IS NOT REPORTED. SUPERSEDES the 2026-10-01 Standing line's opening-word report. The comma's decision reported a listed member written in capitals in a mixed-case name, as the opening word of the part or as the part C4's count flipped, though its writing had settled it (rules/S6); it reports neither now, and a part whose every such word is settled reads in silence wherever they stand. The record is decisions/S6's entry of this date. diff --git a/docs/design/decisions/S3-credentials-written-in-joined-chunks.md b/docs/design/decisions/S3-credentials-written-in-joined-chunks.md index 43643fe18..1b2a485a2 100644 --- a/docs/design/decisions/S3-credentials-written-in-joined-chunks.md +++ b/docs/design/decisions/S3-credentials-written-in-joined-chunks.md @@ -5,7 +5,7 @@ - A dotted token loses its chunk claim only where every chunk the vocabulary matches is a single ASCII character (`i`, `v`, `2`): `John Smith R.A.I.` then reads suffix 'R.A.I.' by shape, with a report, and `Jack X.Y.I.` keeps family 'X.Y.I.', while `Msc.Ed.`, `JD.CPA`, `Lt.Gov.` and `J.씨` keep their chunk readings; retiring every chunk match was declined — 2026-09-15. - A digit-dotted token (`1.4.2`) loses the chunk claim and is no member of the shape class either, so it reads as a name word, silently (`John Smith 1.4.2` → family '1.4.2') — 2026-09-18. - An ambiguous acronym written with its periods, one after each letter or one after each of two or more letter chunks, counts as unambiguous, and a single trailing period does not (`Jack M.A.` → suffix, `Jack Ma.` → family); the clause stood in rules/S2 until it moved here — 2026-10-19. -- Paired initials of a script with capitals (two single letters each followed by a period, the second optional) are no word of the ambiguous class at the trailing slots and read as the name word they are written as, reported, as the switch off reads them: `Ram Bahadur B.K.` → middle 'Bahadur', family 'B.K.'; `Bahadur, Ram B.K.` → middle 'B.K.'; `Ram Bahadur B.K., PhD` → family 'B.K.', suffix 'PhD'. At the first slot after a comma they stay C5's (`John Smith, X.Y. P.Q.` → suffix), behind an unambiguous suffix word they join the run (`Ram Bahadur PhD B.K.` → suffix 'PhD B.K.') while behind an ambiguous or by-shape credential with no unambiguous word in front of it they end the name and take it in (`Jane Doe MA R.T.` → middle 'Doe MA', family 'R.T.'), and three dotted letters, longer chunks and a caseless script's pair keep the position reading (`John Smith X.Y.Z.`, `John Smith B.Tech.`, `สมชาย ใจดี พ.บ.` → suffix) — 2026-10-24 #864. +- Paired initials of a script with capitals (two single letters each followed by a period, the second optional) are no word of the ambiguous class at the trailing slots and read as the name word they are written as, reported, as the switch off reads them: `Ram Bahadur B.K.` → middle 'Bahadur', family 'B.K.'; `Bahadur, Ram B.K.` → middle 'B.K.'; `Ram Bahadur B.K., PhD` → family 'B.K.', suffix 'PhD'. At the first slot after a comma they stay C5's (`John Smith, X.Y. P.Q.` → suffix), behind an unambiguous suffix word they join the run (`Ram Bahadur PhD B.K.` → suffix 'PhD B.K.') while behind an ambiguous or by-shape credential with no unambiguous word in front of it they end the name and take it in (`Jane Doe Ma R.T.` → middle 'Doe Ma', family 'R.T.'; a member its capitals settle starts the run instead, `Jane Doe MA R.T.` → suffix 'MA R.T.', 2026-10-25 #785), and three dotted letters, longer chunks and a caseless script's pair keep the position reading (`John Smith X.Y.Z.`, `John Smith B.Tech.`, `สมชาย ใจดี พ.บ.` → suffix) — 2026-10-24 #864. - A word joined by a hyphen or a slash reads as a suffix where every chunk counts as a suffix as written, listed and not a bare member of the ambiguous class, a chunk's own periods counting as in a word of its own: `John Smith, MD/PhD` → suffix 'MD/PhD', `John Smith MD-PhD` → suffix 'MD-PhD', `John Smith, M.D./Ph.D.` → suffix; one chunk that does not count leaves the word as it was (`John Smith MD-Jones` → family, `John Smith, MA/PhD` → given 'MA/PhD', family 'John Smith'), a word of single-ASCII-character chunks is none (`Jack I-V` → family 'I-V'), a hyphen-joined word written as a hyphenated name is (first chunk in Title case, the rest in Title case or lower case) is a name word (`Kim, Ae-Ra`, `Shin Ae-ra` keep the given and family), while capitals keep the joined reading (`KIM, AE-RA` → suffix), and a joiner standing alone or glued to one side, a dash other than the hyphen-minus and a joined split credential make no joined word — 2026-10-25 #752. Open: none in this file. @@ -20,3 +20,4 @@ Open: none in this file. - 2026-10-25 #752 — JOINED CHUNKS: A WORD JOINED BY A HYPHEN OR A SLASH IS A CREDENTIAL WHERE EVERY CHUNK IS ONE (phase 2c, item 5 of decisions/vocabulary-model's 2026-10-23 #752 entry, agreed by Derek on 2026-10-23). The chunk reading this rule gives interior periods is extended to the two other joiners a credential is written with, by a stricter test: the periods' ANY becomes EVERY, and a chunk must count as a suffix as written, which a bare member of the ambiguous class (`MA`, `Ed`) does not, because a hyphen also joins names (`Smith-Jones`, `Mary-Jane`) and a slash sits between a name and a credential in typed data. The test is asked where a word's suffix reading as written is asked, so classify's tag, the comma's decision and every predicate testing a word's text read the joined word alike. A chunk is read as a word of its own, so a chunk written with its periods counts (`M.D./Ph.D.`, and `M.A./Ph.D.`, the ambiguous member written so). Not read: a joiner standing alone or glued to one side (`MD / PhD` already read as a run of suffix words; `MD/ PhD` keeps `MD/` as a name word, 10 ClinicalTrials.gov entries, and `MD /PhD` 1), the en dash and other dash characters (no entry joins two listed credentials with one), and `&` (63 entries join two listed credentials with it, left for its own change). MEASURED (2026-10-25, against 2af8da8e; every local corpus recorded from that base with `--tree`): `tools/differential/snapshot.py` moves nothing but the rule's own new examples; `cldr_agreement.py` matches every record; `local_agreement.py psc-individuals` matches its record; tse-donors, 0 of 848,351 entries; fec-individuals, 1 of 1,893,964, a doubled joined credential leaving the middle name for the suffix; ctgov-officials, 1,333 of 352,104, the population being the 1,592 words in it joined by a hyphen or a slash whose every chunk counts (the rest already read as suffixes, in a part past a suffix comma). Of the 1,333, 1,319 are a joined credential after a comma that had been read as the given name, the name reversed around it, now the suffix; 2 a joined credential ending a name without a comma, now the suffix where it was the family; and 12 move a report alone, 11 dropping a comma-structure report (a third part now read wholly as credentials) and 1 a suffix-or-name report. Of the 1,321 whose fields move, 3 also take a neighbouring word into the suffix with a new report, each as the unjoined twin reads: a word after the credential (`John Smith, MD/PhD Candidate` reads as `John Smith, PhD Candidate` does) and, once, a capitals credential ending the part before the comma (rules/C12). The joined words are 82 distinct credentials, every one a degree or certification combination, the commonest `MD-PhD` (494), `MD/PhD` (473), `PhD/MD` (65) and `MS-PT` (53); 68 entries hold a chunk written with its periods, which the 2026-10-23 measurement's letters-only test did not read (its 1,254). COST: a word holding a joiner pays three profiler frames at each site that asks its suffix reading as written, stopping at its first chunk that does not count, and a word holding none pays nothing, the joiners being tested in C first (`John Smith` 150 frames before and after, `Mary Smith-Jones` 156 → 159, `Smith-Jones, Mary-Jane` 169 → 175, py3.11). Pinned by the #752 rows of tests/v2/cases.py and `test_suffix_as_written_reads_a_joined_credential` (tests/v2/pipeline/test_vocab.py), whose refused rows pass on the pre-change tree and whose admitted rows fail on it. - 2026-10-25 #752, review of phase 2c — THREE BOUNDARIES THE FIRST VERSION MISSED, found by the PR's code and design-docs reviews. (i) The single-character exception binds a joined word too: `i` and `v` are suffix words, so `Jack I-V` read suffix 'I-V' with no family where 1.4.0 through 2.6.0 read family 'I-V' and the period twin `Jack I.V.` still does; a joined word now needs a chunk that is not a single ASCII character once its own periods are set aside, as a period-split word's chunks are (`Jack I.-V.` keeps family 'I.-V.', the second review's find against the first fix, which counted the period; `John Smith, MS-V` keeps suffix 'MS-V'), and the Except bullet carries the joined rows. No local corpus entry moved back: the measured 1,333 hold no such word. (ii) ACCEPTED: a Korean given name romanized with a hyphen between two syllables the vocabulary lists as unambiguous credentials, `ae` and `ra` (`Kim, Ae-Ra`, `Shin Ae-ra`), reads as the credential, unreported after a comma and leaving no family without one (`Shin Ae-ra` → given 'Shin', suffix 'Ae-ra'). The spaced spelling already did on the base (`Kim, Ae Ra` → suffix 'Ae Ra', `Shin Ae Ra` → suffix 'Ae Ra'), so this is the vocabulary collision decisions/vocabulary-collisions weighs, which the hyphen had been hiding, not a new reading of the joiner; giving `ae` and `ra` a `given` reading, which would make both ambiguous by derivation (decisions/vocabulary-model's 2026-10-25 #752 entry), moves the spaced spelling too and is left for its own change. The reviewer's scan of every romanized Korean syllable found `ae`, `am`, `ra` and `i` counting as suffixes; `i` alone is no joined credential since (i), and no committed or local corpus entry holds a name-cased hyphenated pair of them. (iii) A split credential joined to another (`MD/Ph. D.`) and the dash characters other than the hyphen-minus join nothing, now stated in the rule. - 2026-10-25 #752 (Derek) — A HYPHENATED NAME'S WRITING IS A FORM CUE. SUPERSEDES the review entry's (ii), which accepted `Kim, Ae-Ra` reading suffix 'Ae-Ra'. Derek: "refuse a hyphen join whose every chunk is Title case and then mark AE-RA as the accepted". A word joined by a hyphen whose first chunk is in Title case (one capital, then lower case, its periods set aside) and whose every other chunk is in Title case or lower case is a name word whatever its chunks are listed as, the design's cue that Title case leans to the name (the 2026-10-23 #752 entry's form-cue table, decisions/vocabulary-model). Its review (2026-10-25) narrowed and stated three edges: a word holding a slash anywhere takes no cue (`Kim, Ae-Ra/Md` keeps the joined reading); a one-letter later chunk in lower case is a name's tail like any other (`Md-v`); and a word holding an interior period is read by its periods first, so the cue never sees it (`Kim, Ae.-Ra.` → suffix by the chunk rule, an unwritten shape). The lower-case tail is this change's widening of Derek's wording, because the Revised Romanization writes a given name's second syllable in lower case (`Shin Ae-ra`), the spelling the code review's case was in; `Ae-Ra` and `Ae-ra` both keep the name. Left to the every-chunk test: a chunk with a capital past its first letter (`PhD-Md` → suffix), a chunk in capitals (`MD-PhD`), a word opening in lower case (`md-phd`, the lower-case writing of a credential) and a slash, which joins no name; no local corpus holds a slash-joined credential in Title case, so the slash's exemption is decided by what the cue is for rather than by a count. ACCEPTED in its place: the capitals (`KIM, AE-RA`, `Kim, AE-RA`) and the lower-case writing (`kim, ae-ra`) still read as the credential, a name in capitals having no Title case for the writing to show; the spaced `Kim, Ae Ra` losing its given name is older than phase 2c and stays the `ae`/`ra` vocabulary question. ACCEPTED too: a credential whose chunks are all typed in Title case reads as a name word, reversing the name after a comma (`John Smith, Phd-Md` → given 'Phd-Md'; likewise `Jr-Md`, `Md-Rn`), the writing being the cue; none of the 34 distinct hyphen-joined credential words in the local corpora is written that way (the review's scan, 2026-10-25). MEASURED (2026-10-25): no word in any local corpus joins two listed credentials with a hyphen or a slash in Title case throughout (ctgov-officials, fec-individuals, tse-donors, psc-individuals, ukec-individuals, jrc-names, nepal-candidates; FEC's 31 joined credentials are all capitals), so the cue gives back none of phase 2c's 1,333 ctgov moves; ctgov-officials still moves 1,862 entries over both phases and fec-individuals 1; the committed corpora move only the rules' and cases' new rows. Pinned by `Kim, Ae-Ra` and `Shin Ae-ra` in tests/v2/cases.py, 1.4.0's readings (wheel), and the cue's rows in `test_suffix_as_written_reads_a_joined_credential`. +- 2026-10-25 #785 (Derek, 2026-10-23) — THE ACCEPTED COST OF #864 NARROWS. Paired initials behind a listed member written in capitals in a mixed-case name join the run the member now starts (rules/S8), so `Jane Doe MA R.T.` reads suffix 'MA R.T.', as 2.4.0 through 2.5.0 did, where #864 had read middle 'Doe MA', family 'R.T.'; behind a member the capitals do not settle the pair still ends the name (`Jane Doe Ma R.T.`, `JANE DOE MA R.T.`). The record is decisions/S6's entry of this date. diff --git a/docs/design/decisions/S5-given-parts-trailing-slot-after-a-family-comma.md b/docs/design/decisions/S5-given-parts-trailing-slot-after-a-family-comma.md index b89fab58a..2cf9e425d 100644 --- a/docs/design/decisions/S5-given-parts-trailing-slot-after-a-family-comma.md +++ b/docs/design/decisions/S5-given-parts-trailing-slot-after-a-family-comma.md @@ -4,10 +4,10 @@ Examples name each field as the default order reads it; under the family-first orders the given and family names trade places. -- After a family comma a member of the ambiguous class ending the given part is read by its writing, not the count, and reports whichever way it goes: `Doe, John MA` → suffix 'MA', `Doe, John Ma` → middle 'Ma', `Doe, John X.Y.Z.` → suffix 'X.Y.Z.'; the bare-acronym half restores 1.4.0's role, the dotted half leaves it to agree with `John Doe X.Y.Z.` — 2026-09-18 #531, THE COMMA REPORT'S REACH NOW. +- After a family comma a member of the ambiguous class ending the given part is read by its writing, not the count, and reports whichever way it goes unless its capitals in a mixed-case name settled it, which reports nothing (2026-10-25 #785): `Doe, John MA` → suffix 'MA', `Doe, John Ma` → middle 'Ma', `Doe, John X.Y.Z.` → suffix 'X.Y.Z.'; the bare-acronym half restores 1.4.0's role, the dotted half leaves it to agree with `John Doe X.Y.Z.` — 2026-09-18 #531, THE COMMA REPORT'S REACH NOW. - Accepted with it: one-case and caseless names take the credential with no case evidence (`doe, john ma` → suffix 'ma', `田中, 太郎 MA` → suffix 'MA'), `Doe, John van MA` reads family 'van Doe', suffix 'MA', `Doe, John Prof. MA` gains title 'Prof.', and `DOE, JOHN ED` reads suffix 'ED' — 2026-09-18 #531, THE COMMA REPORT'S REACH NOW. - `do`, the one member that is also a particle, is read by its capitals at this slot: in a mixed-case name `Doe, John DO` reads suffix 'DO', and every other spelling attaches to the family by P6, which reports the fork (`Doe, John do` → family 'do Doe'); the one-case `SMITH, JOHN DO` reading family 'DO SMITH' is the accepted cost, paired with `NASCIMENTO, EDSON ARANTES DO` — 2026-09-18 #531, CAPITALS DECIDE. -- A particle in front of the caps `DO` takes it out of the slot (`Doe, John van DO` → family 'van DO Doe'), and a particle behind a member that is not also an unambiguous suffix word ends the given part where this slot reads it (`Doe, John MA do` keeps middle 'MA'), while `vd` and `mc` are looked past (`Doe, John MA vd` → suffix 'MA', family 'vd Doe') — 2026-09-18 #531, CAPITALS DECIDE. +- A particle in front of the caps `DO` takes it out of the slot (`Doe, John van DO` → family 'van DO Doe'), and a particle behind a member that is not also an unambiguous suffix word ends the given part where this slot reads it (`Doe, John ma do` keeps middle 'ma'; a member its capitals settle starts the credential's run instead, which takes it, `Doe, John MA do` → suffix 'MA do', 2026-10-25 #785), while `vd` and `mc` are looked past (`Doe, John MA vd` → suffix 'MA', family 'vd Doe') — 2026-09-18 #531, CAPITALS DECIDE. - A never-given particle opening the given part leaves no word to spare, so the member behind it is the surname it heads unless mixed-case capitals say credential (`SMITH, VD MA` → family 'SMITH VD MA'; `Smith, de MA` → suffix 'MA') — 2026-10-02 #573. - Two report movements stay as recorded: `van MA` reports only `suffix-or-name`, and `John Smith Ma Ma` reports once; and `John Smith www.example.com` reads suffix 'www.example.com', reported, the dotted shape unable to tell a URL from an acronym — 2026-09-18. @@ -25,3 +25,4 @@ Open: none in this file. - 2026-10-02 (Derek), #573 — a never-given particle opening the given part after a family comma leaves no word to spare; the member behind it is the surname it heads, unless mixed-case capitals say credential. The record is decisions/P9's entry of the same date (decisions/P6's until 2026-10-19). - 2026-10-02 (Derek), #578 — SUPERSEDED by 2026-10-04 #602, decisions/S8, under which the credential starts the given part's run and `Doe, Jane PhD vd Ma` reads suffix 'PhD vd Ma' — A PARTICLE BETWEEN A CREDENTIAL AND A TITLE-CASE WORD STAYS SILENT, AS IT READS. `Doe, Jane PhD vd Ma` reads given 'Jane', middle 'vd Ma', family 'Doe', suffix 'PhD', reporting nothing. No new rule decides that: S2's given-part slot already says a particle in front of a member takes the member out of the slot, and where the word behind reads as a name the two are one name, asked nothing and reporting nothing — `vd` reads as `van` does (`Doe, Jane PhD van Ma`, the same reading), and S2's own boundary example is `Doe, John van Ma`. The writing is malformed whichever reading is meant: a Title-case `Ma` behind a lowercase `vd` behind a `PhD` is neither a Dutch listing nor a credential run as anyone writes one. A reader leaning anywhere leans suffix (Derek: both words follow a known credential, and the writer likely did not know how suffixes are capitalized), but that is a guess about a writer's mistake, not a convention the writing carries. Declined: (a) reading `vd Ma` as suffixes, which needs code to make a credential speak across a particle — against P2 (a particle joins the word behind it) and #554 (`vd` speaks for nothing behind it); (b) reporting the fork, which needs an emitter on the family-comma path for a decision the particle chain makes before assign knows which read the part gets (AGENTS.md: prefer leaving a fork silent and documenting the omission over machinery for input nobody reads with confidence). Where the writing does speak the parser already follows it: `Doe, Jane PhD vd MA` reads suffix 'PhD vd MA' and reports (#573). Pinned by the case row `a_particle_after_a_credential_heads_a_title_case_name`. - 2026-10-17 #682 — SPLIT FROM decisions/S2 with the rule. The entries above were written under decisions/S2 while this rule was part of rules/S2, and moved here unchanged but for pointers the move broke; where one says rules/S2 it means the rule as it then stood, which held this one. +- 2026-10-25 #785 (Derek, 2026-10-23) — A MEMBER ITS CAPITALS SETTLE IS NOT ASKED HERE AS A FORK. At this slot a listed member written in capitals in a mixed-case name already read as the credential; it now reports nothing (rules/S6), and as a run start it takes the words behind it into the suffix (rules/S8): `Doe, John MA Smith` → suffix 'MA Smith' and `Doe, John MA do` → suffix 'MA do', where both read the member as a middle name, and `Doe, John MA Ma` → suffix 'MA Ma'. The rows that pinned the slot's reach and its floor are written in lower case now, which no writing settles, and read as before. 1.4.0 read `Doe, John MA do` and `Doe, John MA Ma` as suffix 'MA, do' and 'MA, Ma'. The record is decisions/S6's entry of this date. diff --git a/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md b/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md index 040cdac90..2edb844a7 100644 --- a/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md +++ b/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md @@ -10,6 +10,8 @@ - `Freiherr von Berg MA` reads family 'von Berg', suffix 'MA', the lean ending P2's chain with no words to spare — 2026-09-15 (Derek). - The whole-PR review's five readings stand: an acronym the particle chain absorbs reports `suffix-or-name` (`John van der Berg Ma`); the CJK stage asks the post-comma run with the one-case fact; a by-shape credential in a tail segment raises no `comma-structure`; a word a caller lists as a surname is never caps-shaped; and a dotted listed member takes the lean — 2026-09-18, WHOLE-PR REVIEW ROUND. +- A listed member of `suffix_acronyms_ambiguous` written in capitals in a name of mixed case is settled as the credential by its writing and reports nothing at any slot that reads it (`John Smith MA`, `Jack MA` but its `given-or-family`, `John Smith, MA`, `Doe, John DO`), and it starts the credential's trailing run (rules/S8: `John Smith MA Jones` → suffix 'MA Jones'); the clash settled is the one with the name reading alone, so the word keeps its tags and its other readings (`Doe, John DO` keeps family 'Doe'), a maiden clause still keeps it as its first word and reports it (`Jane Doe nee MA`), and nothing before a comma leaves it the given name, reported (`, MA`) — 2026-10-25 #785. + Elsewhere: the case a generation word that is also a name reads (rules/S6's bullet on it) is decisions/S11's 2026-10-25 #752 entry. Open: none in this file. @@ -32,3 +34,10 @@ Open: none in this file. - 2026-10-17 #682 — SPLIT FROM decisions/S2 with the rule. The entries above were written under decisions/S2 while this rule was part of rules/S2, and moved here unchanged but for pointers the move broke; where one says rules/S2 it means the rule as it then stood, which held this one. - 2026-10-19 #760 — MOVED FROM the chain emitter's code comment in `_group.py`, the cost record the entries above did not hold. The emitter is written inline against the tags with no new walk — `pieces[j - 1]` is the last piece the merge is about to claim, one index and one frozenset test — so the ordinary chained name (`de la Vega`) pays no frame for it. Its `not is_prefix_piece(last, ...)` floor is tested LAST, behind the `isdisjoint` (a C call, no frame), because it costs a frame (two until group's predicate closures were inlined): asked ahead of the tag test, through the closure, it moved the reference name from 412 to 414 frames, every chained name paying for a question only an ambiguous acronym makes interesting (measured in the #289/#516 review round, 2026-09-18, commit 59d8f38a, interpreter unrecorded; the reference row has since moved for other reasons, decisions/parse-cost). - 2026-10-20 (Derek), #761 — "A COMMA", NOT "A FAMILY COMMA". rules/S6 said the count decides first at the first slot after a family comma, where rules/S4 said after a comma; the parser follows S4 at a suffix comma too (`John Smith, Ma` reads suffix `Ma`, reported). Found in the skeleton migration (PR #783); S6's Precedence lead now says "a comma". No behavior moves. +- 2026-10-25 #785 (Derek, 2026-10-23) — CAPITALS SETTLE A LISTED CREDENTIAL, AND A CLASH SETTLED BY THE WRITING IS NOT REPORTED (#752's phase 2g: the form step of decisions/vocabulary-model's 2026-10-23 #752 entry, item 7, answering #785's first three questions; its fourth was answered there, no new kind). In a name written in more than one case a listed member of the ambiguous class written in capitals was already READ as the credential (the 2026-09-14 entries above); it was still REPORTED as a fork at every slot, while the same word written with its periods (`M.A.`, rules/S3) reported nothing, and it did not start S8's run where the dotted spelling did. #785 asked whether the two writings should answer alike: they do now. `John Smith MA` reports nothing where it reported `suffix-or-name`; `John Smith MA Jones` reads suffix 'MA Jones', reported for 'Jones', where it read middle 'Smith MA', family 'Jones'; a name written in one case keeps its report (`JOHN SMITH MA`), and so does a Title-case member, which leans to the name and is weighed by the count. + HOW, AND WHY NOT A RETAG. The phase 2 spike settled the word by retagging it as an unambiguous credential, and that erased its OTHER readings: `do` is a particle too, and once it stopped being a member of the ambiguous class rules/P6's attachment took it (`Doe, John DO` → family 'DO Doe'), and the maiden clause, whose first word is kept by the clause whatever it is, lost its maiden name (`Jane Doe nee MA`). So the word keeps every tag, and one predicate (`_pieces.settled_by_case`, a listed acronym whose lean is the credential, a suffix word that is also a name being rules/S11's) is asked where a reading is chosen: the trailing peel takes a settled member without recording a pick, so no reporter downstream meets it; the comma's decision and the given part's trailing slot leave its report out; and S8's run start admits it, the particle test in front still refusing `DO` (`John Smith DO Jones` keeps family 'DO Jones'). The case asked is ParseState's one-case fact over the name's own words, so a word in a maiden clause or in brackets is read by the whole name's case, never its own; the spike asked a bracketed word's own case and moved 7 FEC reports for nothing. + WHERE IT STILL REPORTS. Where something before the writing keeps the word a name it is read as before and reported: the first word a maiden clause takes (rules/M2, `Jane Doe nee MA` → maiden 'MA'), and the word after a comma with nothing in front of it, which C4's count leaves the given name (`, MA` → given 'MA'). A bracketed member is rules/S1's, unchanged here (phase 2l). + ACCEPTED. A writer's initials in capitals between the given names and the family name, where they spell a listed member, start the run, and the family name reads as a suffix (rules/S8's Accepted clause, `Maria Ana MA Silva` → suffix 'MA Silva', as `Maria Ana M.A. Silva` already read). In ctgov-officials (fetched 2026-10-21), 9 distinct names write a listed member in capitals between two Title-case name words with no comma, and 6 of them now read this way. The other 5 field moves there are lists of credentials after a comma that the parse reads as a reversed name, a credential taken for the given name (unchanged), whose list behind the member now reads as the suffix where it read as middle names. No name in that shape is written with the periods there. + MEASURED (2026-10-25, Python 3.11.17, against master a46b3a6e, every local check with `--base` that checkout): the committed snapshot moves 54 names, 48 a dropped report and 6 a field, every one a case-row or rules text, and gains 20 texts and loses 1, the rows added and those rewritten to keep reaching their emitter; every CLDR, GND, Wikidata and locale agreement record matches. ctgov-officials moves 1,778 readings: 1,767 drop a report and nothing else, each a capitals listed member (`DO` 1,090, `MA` 426, `BA` 184, `JD` 35, `PA` 11, the rest five or fewer; by shape `Aa Aa, DO` 607, `Aa Aa Aa, DO` 244, `Aa Aa, MA` 176), and 11 move a field, each the run a settled member starts (the 6 accepted above and the 5 credential lists); none gains a report. fec-individuals, written in one case, and tse-donors match the base; psc-individuals matches it on every record (1,004,785 of 1,039,171 agreeing). The phase 2 spike's figures (71 committed, 1,853 ctgov, 7 FEC) were taken on another base with the retag, and are not this change's. + FRAMES (`sys.setprofile` call events over a second parse, py3.11): the reference row and plain names cost what they did (`Dr. Juan0001 de la Vega III` 305, `John Smith` 149, `Smith, John` 158, `John Smith, PhD` 202); a name holding a member pays for the predicate where it is asked (`John Smith Ma` 213 → 220, `JOHN SMITH MA` 212 → 215, `John Smith MA` 212 → 203, the pick and its report gone) and for the run it now reads (`John Smith MA Jones` 232 → 259). + Recompute: `uv run python tools/differential/snapshot.py --since `; for each local corpus `snapshot.py --local --base `, and `local_agreement.py psc-individuals --base `; the ctgov shape count by reading `corpus.jsonl` of ctgov-officials, keeping each distinct name whose part before its first comma holds, after two words, a word whose letters are a listed member written in capitals between a Title-case word and the next, and counting those whose suffix holds it, on this tree and on the parent. diff --git a/docs/design/decisions/S8-credentials-trailing-run.md b/docs/design/decisions/S8-credentials-trailing-run.md index 2eb667391..61590d88e 100644 --- a/docs/design/decisions/S8-credentials-trailing-run.md +++ b/docs/design/decisions/S8-credentials-trailing-run.md @@ -6,17 +6,17 @@ Examples name each field as the default order reads it; under the family-first o - An unambiguous suffix word of two or more letters, in any script, standing after two name words with no comma or before a suffix comma, or after the given word in the part after a family comma, starts a run to the end of its part: every later word reads as a suffix except a title word, which reads as a title, and an absorbed name word reports `suffix-or-name` (`John Smith PhD Jones` → suffix 'PhD Jones'; `Eric H. Holder Jr. Attorney General` → suffix 'Jr.', title 'Attorney General') — 2026-10-04 #602; 2026-10-14 #679. - A roman numeral of two or more letters that reads as the generation starts the run as a listed one does (`John Smith VI Jones` → suffix 'VI Jones', as `John Smith III Jones`), its shape alone reported — 2026-10-25 #784. -- What never starts the run: a title word (`Mary Jane King Smith` keeps family 'Smith'), a member of the ambiguous class but a credential split across a space (below), a single letter, a connective (`Josep Carod i Rovira` keeps family 'Carod i Rovira'), and a word that is also particle or bound given-name vocabulary (`Mohamed Ali Abd Allah` → family 'Allah'); a title/suffix dual does start it — 2026-10-04 #602. +- What never starts the run: a title word (`Mary Jane King Smith` keeps family 'Smith'), a member of the ambiguous class but a credential split across a space (below) and a listed member its capitals settle (below), a single letter, a connective (`Josep Carod i Rovira` keeps family 'Carod i Rovira'), and a word that is also particle or bound given-name vocabulary (`Mohamed Ali Abd Allah` → family 'Allah'); a title/suffix dual does start it — 2026-10-04 #602. - An honorific or generational word starts the run as a credential does, whatever its script (`राम शर्मा साहब मोहन` → suffix 'साहब मोहन'); the one-character honorifics stay out by the two-letter minimum — 2026-10-14 #679. - A lone particle counts toward the two name words only where the credential stands right behind it (`John von PhD Jones` → family 'von', suffix 'PhD Jones', under the family-first orders given 'von'); a particle run counts once, and a particle the run took is not asked (`Freiherr von vd PhD Jones` keeps middle 'PhD') — 2026-10-10 #627. - Lifting the credential out of the name, the family-comma path's older reading (`John Smith PhD Jones` as family 'Jones', suffix 'PhD'), was declined — 2026-10-04 #602. -- An ambiguous acronym written with its periods starts the run (`John Smith M.A. Jones` → suffix 'M.A. Jones'); its capitals alone do not (`John Smith MA Jones` → middle 'Smith MA', family 'Jones') — 2026-10-20 #761. +- An ambiguous acronym written with its periods starts the run (`John Smith M.A. Jones` → suffix 'M.A. Jones'); and so, since 2026-10-25 #785, does a listed member written in capitals in a name of mixed case, its writing settling it (`John Smith MA Jones` → suffix 'MA Jones'; `John Smith Ma Jones` and `JOHN SMITH MA JONES` keep family 'Jones', and `John Smith DO Jones` family 'DO Jones', its particle reading refusing the start) — 2026-10-20 #761; 2026-10-25 #785. Not standing: the split of 2026-10-17 #682, carried out. Elsewhere: the split `Ph. D.` starting the run in a main segment too, from where it stands though the walk leaves it out, and any other credential split across a space starting it as the one member of the ambiguous class that does (`John Smith Ph. D. Jones` → suffix 'Ph. D. Jones', reported; `John Smith Pharm D Jones` → suffix 'Pharm D Jones') — decisions/S12's 2026-10-25 #752 entry; a credential opening the part after the comma, and the split `Ph. D.` starting the run as `PhD` does (`Smith, John Ph. D. Jones` → suffix 'Ph. D. Jones') — decisions/C13's 2026-10-04 (Derek), #603 entry; the run stopping short of a lone letter ending the given part ahead of a further comma (`Smith, John PhD I, Jr.` → middle 'I', suffix 'PhD, Jr.') — decisions/C9's 2026-10-25 #819 entry. -Open: [#785](https://github.com/derek73/python-nameparser/issues/785) (whether capitals in a mixed-case name start the run as periods do). +Open: none in this file. ## Log @@ -33,3 +33,4 @@ Open: [#785](https://github.com/derek73/python-nameparser/issues/785) (whether c - 2026-10-17 #682 — SPLIT FROM decisions/S2 with the rule. The entries above were written under decisions/S2 while this rule was part of rules/S2, and moved here unchanged but for pointers the move broke; where one says rules/S2 it means the rule as it then stood, which held this one. - 2026-10-20 (Derek), #761 — AN AMBIGUOUS ACRONYM WRITTEN WITH ITS PERIODS STARTS THE RUN. The skeleton migration (PR #783) found S8 ("a member of the ambiguous class" never starts the run) and S3 (an ambiguous acronym written with its periods "counts unambiguously") both claiming `John Smith M.A. Jones`, with no text naming the winner; the parser reads suffix `M.A. Jones`. Derek: "writing periods makes it unambiguously not a name". S3 wins, written as a Precedence bullet in rules/S8; no behavior moves. The bullet is scoped to the periods: the capitals alone do not start the run (`John Smith MA Jones` reads middle `Smith MA`, family `Jones`), a question tracked in [#785](https://github.com/derek73/python-nameparser/issues/785). - 2026-10-25 #784 — A NUMERAL THAT READS AS THE GENERATION STARTS THE RUN, WHATEVER ITS SPELLING. The run start asked the suffix vocabulary, so a listed numeral started it and a numeral S10 reads by its shape did not: `John Smith III Jones` → suffix 'III Jones', `John Smith VI Jones` → family 'Jones'. It now asks the numeral's predicate (decisions/vocabulary-model's 2026-10-25 2f entry) for a numeral of two or more letters, with the word in front and the name's case, so a Title-case `Vi` in a mixed-case name, which leans to the name, and a numeral behind an initial start nothing. A single letter still never starts the run. The numeral starting the run reports `suffix-or-name` where its shape alone reads it, as S10's numeral does anywhere it is weighed. The given part after a family comma starts its run the same way (`Smith, John VI Jones`). Separately, #819's reading of a lone letter ahead of a further comma stops the run short of that letter (decisions/C9's 2026-10-25 #819 entry). +- 2026-10-25 #785 (Derek, 2026-10-23) — A LISTED MEMBER ITS CAPITALS SETTLE STARTS THE RUN, as its dotted spelling has since 2026-10-20 #761, answering #785's second question. Asked of the run start's one predicate (`_pieces.starts_a_credential_run`), whose particle test still refuses `DO`, so a member's other readings answer as before; the inline pre-tests ahead of it (the trailing run's start and the given part's) admit a member, as they admit a numeral. Accepted: a writer's initials in capitals between the given names and the family name that spell a listed member start the run too (rules/S8's Accepted clause). The record, with what was measured, is decisions/S6's entry of this date. diff --git a/docs/design/decisions/vocabulary-model.md b/docs/design/decisions/vocabulary-model.md index 3ae24e434..75c97a862 100644 --- a/docs/design/decisions/vocabulary-model.md +++ b/docs/design/decisions/vocabulary-model.md @@ -17,6 +17,7 @@ The vocabulary was a set of base sets plus marker subsets (`particles` and `part - Phase 2 is to put vocabulary, declared name and shape readings in one pool, derive `may_be_given` and `may_be_name` from declared name readings (a count-free one carrying a `note`, `ld` left ungated, `may_be_initial` kept), and decide every clash by one gate of four steps, floor, form, structure and count, asked at the sites that decide today; the reading not taken is to surface as `Ambiguity.alternative` in 2.x and the reading taken as `Token.reading` in 3.0 — 2026-10-23 #752. - Every numeral rule asks one predicate, `_vocab.reads_as_generation`, the vocabulary's own numerals read inside it; a numeral of two or more letters is the generation wherever the trailing run holds it, while a one-letter one before a credential stays the name word decisions/S10's #721 entry made it — 2026-10-25 #752, phase 2f. - A credential written split across a space is one word, a member of the ambiguous class that the count decides, and the Ph./D. pair starts S8's run from where it stands (decisions/S12) — 2026-10-25 #752, phase 2e. +- A listed credential that is also a name, written in capitals in a name of mixed case, is settled as the credential by its writing: it reports nothing and starts the credential's run, and keeps its other readings, the clash settled at the sites that choose a reading rather than by retagging the word (decisions/S6) — 2026-10-25 #752, phase 2g, #785. - Phase 2's title batches go in order of lone-head breakage in PSC, `buddha`, `siddha` and `gyani` in the default gate and `guru` left to a `ne` pack — 2026-10-24 #752. - `Dean Smith` reads given `Dean`. The contested-head gate is to cover the words outside `titles` (`dean`, `justice`, `major`), and a title that is also a given name gets a declared given reading one word at a time, in measured batches — 2026-10-15/16; 2026-10-23 #752. - Order and shape are separate: a language supplies only a default order, and the shapes (where titles, suffixes, particles and commas sit) belong to each order and live in the core; vocabulary readings are to settle a clash and name readings only to keep the gate on — 2026-10-15/16. @@ -143,7 +144,7 @@ Open: #752 (phase 2, derived ambiguity; its sub-phases 2a to 2m, each its own PR 4. **"One gate for six rules" is four clashes and two floors.** The title run's floor (rules/H3) and bound given names (rules/P5) reach every member of their category and read only a count; no second reading is weighed, so they are step 1, unchanged. The acronym gate's nine decision sites stay nine, each keeping the order it reads its cues in (C4 the count before the case, S6 the case before the count); what becomes one is the predicate each asks. 5. **Joined credentials land: a word joined by `-` or `/` whose every chunk is listed and none ambiguous reads as a credential, and an unlisted word of capitals so joined, with a chunk of two or more letters, joins the ambiguous class after a comma, gated by C6's case contrast.** The stricter chunk test is because a hyphen also joins names (`Smith-Jones`). Together they moved 1,782 distinct ctgov readings (1,254 and 759, overlapping on 231), nothing in PSC, the committed corpora or the agreement records; FEC is one case, so the contrast never admits a word there. 6. **A split credential merges only where its first chunk has two or more letters, and the merged piece is a member of the ambiguous class, decided by C4's count and S4's words to spare; it starts S8's run (#827), landing after the numeral predicate answers the numeral before a credential.** FEC decided the opening letter: it writes a middle initial before a trailing salutation (`SMITH, JOHN A. MS.`), and `A. MS.` folds to the listed `ams` (merging any pair ending its part, but for two single letters, moved 5,122 FEC names, `A. MS.` 2,246 of them). A two-letter opening chunk needs the count: the 30 FEC merges it made all stood behind a one-word family (`SAM E.` → `same`), against 351 of 352 ctgov merges behind two or more name words. Accepted cost: `M. D.` and `M D` stay unmerged. Left to the split-credential PR: rules/S8 refuses a member of the ambiguous class as a run start, so how a merged piece can be both is that PR's to state (today's `Ph. D.` merge is unambiguous, and S8's #827 row covers it). (Carried out, and the run start stated, by the 2026-10-25 #752, phase 2e entry below, decisions/S12. Corrected there too: `M. Sc.` opens with a single letter as well and stays unmerged, so the tally's figure of officials still reversing left it out.) - 7. **A word taken into the credential's run by position alone (`John Smith PhD Jones`) gets no new ambiguity kind.** It is not a clash between two readings; the surfaced reading's source tells it from a word with a declared name reading (item 13). This answers #785's fourth question; its first three (case drops the report and starts the run, the one-case row keeps its report) land through the pool, since retagging the word was measured to erase its other readings (`Doe, John DO` became family `DO Doe` once `do` stopped being an ambiguous acronym, its particle reading meeting P6). + 7. **A word taken into the credential's run by position alone (`John Smith PhD Jones`) gets no new ambiguity kind.** It is not a clash between two readings; the surfaced reading's source tells it from a word with a declared name reading (item 13). This answers #785's fourth question; its first three (case drops the report and starts the run, the one-case row keeps its report) land through the pool (carried out by phase 2g, the 2026-10-25 entry below), since retagging the word was measured to erase its other readings (`Doe, John DO` became family `DO Doe` once `do` stopped being an ambiguous acronym, its particle reading meeting P6). 8. (narrowed by the 2026-10-25 #752, phase 2f entry: in the given part after a family comma a one-letter numeral the vocabulary lists stays the suffix, and a one-letter numeral before a credential stays a name word) **Every numeral rule asks one predicate, answering for `i` to `x` the way S10's trailing fork does (shape, the count for one letter, an initial before it, the period, the case), with three readings stated:** a lone letter ending the given part ahead of a further comma is the initial (#819; `Smith, John PSM I, Jr.` the accepted cost), a lone letter in a given part after a family comma stays the initial, and a Title-case numeral in a name of mixed case leans to the name. It answers #784 (`John Smith VI MD` reads family `VI` where `III` starts the run) and #774. 9. **Generation readings are gated by their kind's cue, and a reading with none is `default = false`, compiled to nothing by default and turned on by a pack in phase 3.** `fils` is gated by case and the count. The Brazilian agnomens `Filho`, `Filha`, `Neto`, `Neta`, `Netto`, `Sobrinho` and `Sobrinha` become gated generation readings in `pt.toml` (`pt-BR` where only Brazil writes one), with the count as their only cue: they are written in the case of the name around them. In tse-donors, 405 filed/registry pairs differ only by a trailing agnomen; master gives both spellings the same last family word in 1, and a gate emulated as an ambiguous acronym, so carrying S6's Title-case lean beside the count, in all 405, keeping every PSC surname (`Ana Neto`) and FEC given name (`SMITH, NETO`) the plain reading broke, by the count and the lean. The count-only gate the design chooses was not measured: it is to be measured before 2h lands. They are not part of the surname: a surname is what a family shares, and an agnomen marks the bearer against a relative of the same name, as `Júnior` does. Spanish `H.` (*hijo*), German regnal `I.` and Czech `st.` are `default = false`. When 2h lands it replaces decisions/generational-words' Standing line that the agnomens stay family names. 10. **Decision 10 is unparked at the head of the name only: a modifier is a title only beside a head title there, never in a comma part,** after `resident` and `rt` move to `occupation` and `pro` to `academic`. At the head it fixed 177 PSC records and broke 7; in a comma part it reverses names whose part after the comma is the person's role (247 of 367 ctgov comma-part moves, the part being exactly `Resident` 120 times and `resident` 50, counted as the family taking master's given name). @@ -205,3 +206,5 @@ Open: #752 (phase 2, derived ambiguity; its sub-phases 2a to 2m, each its own PR - `default = false` is a behavioral property of every category but `name`: the reading is held to the per-entry checks and compiles into no set and no marker, and checks 4, 5, 6 and 9 do not weigh it, so it may stand beside a live reading of the word in another file and beside another language's exclusion of it. `default = true` is refused, being every reading's unwritten value. The first three are the readings with no cue: Spanish `h` (*hijo*) and the German regnal ordinal on `i`, each written with the period that makes a one-letter word an initial (rules/T5), and Czech `st` (*starší*), whose `cs.toml` exclusion it replaces. No reading moves. - The other reserved names keep their reservation and name the phase that needs them: `attaches` phase 2 (its front-of-the-name work), `prefer` and `strict` phase 3, and `bare_only`, `leading_only`, `trailing_only` and `separators` phase 4, the gate's slots carrying position and shape settling a bare letter (rules/P7). Adding the two fields changes `Lexicon`'s pickle layout, as 2.6 did. + +- **2026-10-25 #752, phase 2g — CAPITALS SETTLE A LISTED CREDENTIAL**, carrying out item 7 of the 2026-10-23 #752 entry above (#785's first three questions). In a name written in more than one case a listed member of the ambiguous class written in capitals reports nothing where it read as the credential (`John Smith MA`, `John Smith, MA`, `Doe, John DO`), and it starts the credential's run as its dotted spelling does (`John Smith MA Jones` → suffix 'MA Jones'). The word keeps its tags, so the clash settled is the one with the name reading alone: `Doe, John DO` keeps family 'Doe', the particle reading still refusing the run start, and `Jane Doe nee MA` keeps its maiden name and its report. A name in one case keeps its report (`JOHN SMITH MA`). The record, with what was measured and the accepted cost, is decisions/S6's entry of this date (rules/S6, S8). diff --git a/docs/design/rules/C11-what-the-comma-decision-reports.md b/docs/design/rules/C11-what-the-comma-decision-reports.md index f2704d56c..b615391a1 100644 --- a/docs/design/rules/C11-what-the-comma-decision-reports.md +++ b/docs/design/rules/C11-what-the-comma-decision-reports.md @@ -8,9 +8,10 @@ ``` "John Smith, A.B." → ambiguities=("suffix-or-name",) ``` -- **A run the count leaves in the listing form reports as S7 reads the words in it**: a word of the class read as the credential because a credential in front speaks for it reports, and one its own capitals made the credential does not unless it opens the part, where it is the first word after the comma and reports as that word always does ('Doe, MA PhD', 'John Smith, MA MA'). So a run whose every such word is written in capitals reads whole in silence only behind another credential ('John Smith, PhD MA', 'Smith, PhD MA'), as does a part read as titles before a lone given name ('Smith, Ms RA Ma'). +- **A run the count leaves in the listing form reports as S7 reads the words in it**: a word of the class read as the credential because a credential in front speaks for it reports, and one its own capitals made the credential does not, wherever it stands in the part (S6). So a run whose every such word is written in capitals reads whole in silence ('Doe, MA PhD', 'John Smith, MA MA', 'John Smith, PhD MA'), as does a part read as titles before a lone given name ('Smith, Ms RA Ma'). ``` - "Doe, MA PhD" → ambiguities=("suffix-or-name",) + "Doe, MA PhD" → ambiguities=() + "Doe, Ma PhD" → ambiguities=("suffix-or-name",) "John Smith, PhD DO DO" → ambiguities=() · boundary ``` - **It is one of TWO places the comma's own decision is reported**, the other being the word trailing the given part after it (S5), which is a second decision about a second word and never the same fork twice; an attachment decided after a family comma (P6) reports on its own. @@ -26,6 +27,11 @@ **Except:** +- **A decision that the capitals of every listed word of the class in the part settled is not reported**, since the writing left the reader nothing to weigh (S6); in a name written in one case the same part reports. + ``` + "John Smith, MA" → ambiguities=() + "JOHN SMITH, MA" → ambiguities=("suffix-or-name",) + ``` - **A flip in which no listed word of the ambiguous credential class (C4) takes part is the exception and is made in silence**, since only paired initials among such words are ever taken for a name, and the flip reaches them only where something else in the part has said otherwise — unless what said so is nothing but other paired initials: each of those is a word a reader takes for a name, and that flip is reported. ``` "John Smith, Jr vd" → ambiguities=() diff --git a/docs/design/rules/M2-maiden-marker-rule.md b/docs/design/rules/M2-maiden-marker-rule.md index c4eeb37cf..504f1e6e9 100644 --- a/docs/design/rules/M2-maiden-marker-rule.md +++ b/docs/design/rules/M2-maiden-marker-rule.md @@ -42,7 +42,12 @@ "Jane Doe nee Smith PhD Smith" → suffix="PhD Smith" "Jane van der Berg née y Jones" → maiden="y Jones" ``` -- **What reports.** Where a trailing rule reads the part, a member of the ambiguous class (S4) that ends the maiden name is reported where the clause keeps it, and one in the run is reported where the run is read, so no word is reported twice and none goes unreported. +- **What reports.** Where a trailing rule reads the part, a member of the ambiguous class (S4) that ends the maiden name is reported where the clause keeps it, whatever its case, and one in the run is reported where the run is read unless its capitals settled it (S6), so no word is reported twice and none the writing left open goes unreported. + ``` + "Jane Doe nee MA" → ambiguities=("suffix-or-name",) · boundary + "Jane Doe nee Smith MA" → ambiguities=() + "Jane Doe nee Smith Ma JD" → ambiguities=("suffix-or-name",) + ``` - **A marker may be more than one word**, and is then recognized only whole and only where its words stand together: its own first word standing without the rest is not that marker, and neither is one the writer divided from the rest by a bracketed clause or by a comma. Whether the first word takes anything on its own is a separate question with a separate answer — it does exactly when it is a recognized marker in its own right, and the longer entry wins wherever both could match. ``` "Maria Kowalska z domu Nowak" → maiden="Nowak" diff --git a/docs/design/rules/S3-credentials-written-in-joined-chunks.md b/docs/design/rules/S3-credentials-written-in-joined-chunks.md index a2a6003d7..5b89e8f27 100644 --- a/docs/design/rules/S3-credentials-written-in-joined-chunks.md +++ b/docs/design/rules/S3-credentials-written-in-joined-chunks.md @@ -87,6 +87,10 @@ "John Smith, MA/PHD" → suffix="MA/PHD" ``` - **An ambiguous credential between an unambiguous one and paired initials** ("Jane Doe PhD MA R.T."): the unambiguous word starts the run and the run takes both, the pair included (S8). +- **An ambiguous credential its capitals settle, in front of paired initials** ("Jane Doe MA R.T."): it starts the run as an unambiguous one does, and the run takes the pair (S6, S8). + ``` + "Jane Doe MA R.T." → suffix="MA R.T." + ``` - **Paired initials at the first slot after a comma** ("García Márquez, G.J."): they read as the given name unless something in the part speaks for them (C5). - **A flip to the credential run at the first slot after a comma** ("John Smith, X.Y.Z."): where no listed member of the class takes part it is not reported, unless paired initials speak only for each other (C11). @@ -111,10 +115,11 @@ ``` "John Smith K.C." → family="K.C." ``` -- Paired initials behind an ambiguous credential that no unambiguous suffix word stands in front of, a listed one or one admitted by its shape (this rule's dotted words, S9's capitals where the caller admits them), end the name and take the credential into it: the reading 1.4.0 through 2.3.0 gave, where a run of credentials is the likelier writing. +- Paired initials behind an ambiguous credential that no unambiguous suffix word stands in front of, a listed one or one admitted by its shape (this rule's dotted words, S9's capitals where the caller admits them), and whose own writing does not settle it (S6), end the name and take the credential into it: the reading 1.4.0 through 2.3.0 gave, where a run of credentials is the likelier writing. ``` - "Jane Doe MA R.T." → family="R.T." - "Jane Doe MA R.T." → middle="Doe MA" + "Jane Doe Ma R.T." → family="R.T." + "Jane Doe Ma R.T." → middle="Doe Ma" + "JANE DOE MA R.T." → family="R.T." ``` - The initialless-script clause carries no example line of its own. Every input that exercises it composes a script that writes no abbreviations with a period that only a Latin convention writes — `John Smith 田.中.` is the shape — and a composed form no writing system produces is tolerated input rather than contract, which an example of a normative rule would promise. So the witnesses are the tolerated row tests/v2/cases.py's an_initialless_script_glued_into_periods_is_not_this_shape and the CJK assertions in tests/v2/pipeline/test_vocab.py's test_period_joined_vocab_retires_the_single_character_chunk. W3 states the same precedence for the same reason. diff --git a/docs/design/rules/S4-ambiguous-acronym-needs-words-to-spare.md b/docs/design/rules/S4-ambiguous-acronym-needs-words-to-spare.md index 24c4a671a..941617d8c 100644 --- a/docs/design/rules/S4-ambiguous-acronym-needs-words-to-spare.md +++ b/docs/design/rules/S4-ambiguous-acronym-needs-words-to-spare.md @@ -2,7 +2,7 @@ **When:** a bare acronym that is also an ordinary name (MA, Ma) stands at a slot where a suffix may stand, as in "John Ma". -**Then:** a BARE ambiguous acronym is consumed only when the name has words to spare — as the second of two words it stays the family name — and at the slots that report, either reading carries the ambiguity flag. +**Then:** a BARE ambiguous acronym is consumed only when the name has words to spare — as the second of two words it stays the family name — and at the slots that report, either reading carries the ambiguity flag, unless the word's writing has settled it (S6). - **The slots that report** are the trailing slot of a name, the first slot after a family comma, the trailing slot of the GIVEN part after that comma, and the trailing slot of a maiden marker's clause (M2). A part beyond the second comma is not one of them; what such a part does report is C2's to say. - **A word this document says is READ at one of those slots is not always a word one of them reports**: where a reading moves a word out of the slot that asked about it, what reports is the slot it lands in, and that may be none — M2 states the case where a clause does it. diff --git a/docs/design/rules/S5-given-parts-trailing-slot-after-a-family-comma.md b/docs/design/rules/S5-given-parts-trailing-slot-after-a-family-comma.md index 7e39fc631..52705aaae 100644 --- a/docs/design/rules/S5-given-parts-trailing-slot-after-a-family-comma.md +++ b/docs/design/rules/S5-given-parts-trailing-slot-after-a-family-comma.md @@ -15,15 +15,18 @@ "Doe, John DO" → suffix="DO" "Doe, John Ma" → middle="Ma" · boundary ``` -- **How far "ending the given part" reaches.** It reaches past the credentials behind it and past a trailing title, which is transparent to this reading as it is to the rest of the trailing reading (H5); a name word behind the word ends the reach, and so does a PARTICLE the suffix vocabulary does not also claim as an unambiguous suffix word (`do` is in its ambiguous half): it belongs to the family the comma already named and is taken there by a rule that runs after this reading is made (P6), so `Doe, John MA do` keeps its middle name though the capitals would otherwise have taken the word. A particle the suffix vocabulary DOES claim is looked past like any other credential, so `Doe, John MA vd` reads suffix `MA`, with `vd` attaching behind it. Where the reach ends, the word is an ordinary middle name, read in silence. +- **How far "ending the given part" reaches.** It reaches past the credentials behind it and past a trailing title, which is transparent to this reading as it is to the rest of the trailing reading (H5); a name word behind the word ends the reach, and so does a PARTICLE the suffix vocabulary does not also claim as an unambiguous suffix word (`do` is in its ambiguous half): it belongs to the family the comma already named and is taken there by a rule that runs after this reading is made (P6), so `Doe, John Ed do` keeps its middle name. A word its capitals settle in a name of mixed case is not reached that way: it starts the credential's run, which reads every word behind it as a suffix (S8), so `Doe, John MA Smith` and `Doe, John MA do` read as `Doe, John PhD Smith` and `Doe, John PhD do` do. A particle the suffix vocabulary DOES claim is looked past like any other credential, so `Doe, John MA vd` reads suffix `MA`, with `vd` attaching behind it. Where the reach ends, the word is an ordinary middle name, read in silence. ``` - "Doe, John MA Smith" → middle="MA Smith" · boundary + "Doe, John Ma Smith" → middle="Ma Smith" · boundary + "Doe, John MA Smith" → suffix="MA Smith" + "Doe, John MA do" → suffix="MA do" ``` -- **What reports.** Every word this slot does read reports the fork whichever way it went, so a run of members all read as credentials reports once for each, as the same words do without the comma — while a member the writing keeps as a name stops the reading there, and whatever stands in front of it is name text, asked nothing and reporting nothing. +- **What reports.** Every word this slot does read reports the fork whichever way it went, unless its capitals settled it (S6), so a run of members all read as credentials reports once for each, as the same words do without the comma — while a member the writing keeps as a name stops the reading there, and whatever stands in front of it is name text, asked nothing and reporting nothing. ``` - "Doe, John MA JD" → ambiguities=("suffix-or-name", "suffix-or-name") - "Doe, John MA Ma" → middle="MA Ma" · boundary - "Doe, John MA Ma" → ambiguities=("suffix-or-name",) · boundary + "DOE, JOHN MA JD" → ambiguities=("suffix-or-name", "suffix-or-name") + "Doe, John MA JD" → ambiguities=() + "Doe, John ma Ma" → middle="ma Ma" · boundary + "Doe, John ma Ma" → ambiguities=("suffix-or-name",) · boundary ``` **Because:** a family comma has already named the family and the given name, so at the end of the given part the words-to-spare count has nothing to say, and only the writing can. @@ -36,7 +39,7 @@ "smith, de ma" → family="smith de ma" "SMITH, VD DE LA MA" → family="SMITH VD DE LA MA" ``` -- **A particle in FRONT of the word** takes it out of this slot as well, and takes the word behind it too: where what follows the particle reads as a name rather than a credential the two are one name, so neither word is asked and neither reports — `Doe, John van Ma` reads middle `van Ma` and `Doe, John DO Ed` middle `DO Ed`, where `Doe, John van MA` reads suffix `MA` and `Doe, John DO` alone reads the credential, both reporting. +- **A particle in FRONT of the word** takes it out of this slot as well, and takes the word behind it too: where what follows the particle reads as a name rather than a credential the two are one name, so neither word is asked and neither reports — `Doe, John van Ma` reads middle `van Ma` and `Doe, John DO Ed` middle `DO Ed`, where `Doe, John van MA` reads suffix `MA` and `Doe, John DO` alone reads the credential, both settled by their capitals (S6). ``` "Doe, John van Ma" → middle="van Ma" · boundary "Doe, John van Ma" → ambiguities=() · boundary diff --git a/docs/design/rules/S6-case-signal-at-the-suffix-slot.md b/docs/design/rules/S6-case-signal-at-the-suffix-slot.md index 16245860c..b16996bb0 100644 --- a/docs/design/rules/S6-case-signal-at-the-suffix-slot.md +++ b/docs/design/rules/S6-case-signal-at-the-suffix-slot.md @@ -4,9 +4,19 @@ **Then:** written case is evidence beside the count (S4), and it speaks only in a name written in more than one case: there a member of the ambiguous set written in capitals reads as the credential even with no words to spare, and one written in any other cased form that is not wholly lower reads as the name even with words to spare. -- **A name written wholly in one case says nothing about any word in it**, and the count decides alone; so does a script with no case to write in. +- **A clash settled by the writing is not reported**: a listed member its capitals read as the credential is no longer a word that might be either, as one written with its periods is not (S3), so no `suffix-or-name` report names it at any slot S4, S5 or C4 reads it. Only the clash with the name reading is settled: the word keeps its other readings, so a particle reading (`DO`) answers P6 as before, and a member's capitals start the credential's trailing run as a credential's do (S8). + ``` + "John Smith MA" → ambiguities=() + "Jack MA" → ambiguities=("given-or-family",) + "John Smith, MA" → ambiguities=() + "Doe, John DO" → family="Doe" + "Doe, John DO" → ambiguities=() + "John Smith Ma" → ambiguities=("suffix-or-name",) + ``` +- **A name written wholly in one case says nothing about any word in it**, and the count decides alone and reports as S4 says; so does a script with no case to write in. The case asked of a word in a maiden clause or between brackets is the whole name's, the clause and the brackets being no name of their own. ``` "JACK MA" → family="MA" · boundary + "JOHN SMITH MA" → ambiguities=("suffix-or-name",) ``` - **At the trailing slot of the given part** after a family comma, the comma has already settled the count, so the writing is the only evidence there is (S5). - **A generation word that is also a name (S11) is written the other way at one end**: its lower case in a name of mixed case is the generation wherever the name keeps a family word, never with none to spare (S11), its capitals say nothing, a surname being often written in them, and its Title case leans to the name as an acronym's does. An agnomen's writing says nothing at all, since it takes the case of the name around it. @@ -28,6 +38,15 @@ "john smith MEng" → family="MEng" ``` +**Except:** + +- **Where something before the writing keeps the word a name, it reports as before**: with nothing in front of a comma C4's count leaves the word after it the given name, the case being read only behind one name word (C4), and the first word after a maiden marker is the maiden name the marker announced (M2), whatever case either is written in. + ``` + ", MA" → given="MA" · boundary + ", MA" → ambiguities=("suffix-or-name",) · boundary + "Jane Doe nee MA" → ambiguities=("suffix-or-name",) · boundary + ``` + **Precedence:** - **The first slot after a comma** ("Smith, Ma", "John Smith, Ma"): this evidence is SECOND there: the count of name words before the comma decides there first (C4), and the case is read only where that count leaves the word a name. @@ -35,8 +54,8 @@ "Smith, MA" → suffix="MA" "Smith, Ma" → given="Ma" ``` -- **A part of listed members written in capitals, at the first slot after a comma** ("Smith, MA MA"): C4 states the one exception: a part of two or more suffix words and words of this class, every word of the class listed and written in capitals in a mixed-case name, is read on its capitals before the count is asked, as the credential run behind one name word as behind two ('Smith, MA MA' reads suffix 'MA MA'), and only a word of the class opening the part reports, as the first word after the comma ('John Smith, MA MA' reports on the first 'MA' alone, where a counted flip reports over the whole part; the reports are C11's). +- **A part of listed members written in capitals, at the first slot after a comma** ("Smith, MA MA"): C4 states the one exception: a part of two or more suffix words and words of this class, every word of the class listed and written in capitals in a mixed-case name, is read on its capitals before the count is asked, as the credential run behind one name word as behind two ('Smith, MA MA' reads suffix 'MA MA'), and the writing having settled every word of the class in it, the part reports nothing (C11). --- **Signal:** case contrast: capitals on a word of the ambiguous class in a name written in more than one case -**history:** decisions/S6 · **implemented:** nameparser/_pipeline/_vocab.py, nameparser/_render.py +**history:** decisions/S6 · **implemented:** nameparser/_pipeline/_assign.py, nameparser/_pipeline/_comma.py, nameparser/_pipeline/_pieces.py, nameparser/_pipeline/_vocab.py, nameparser/_render.py diff --git a/docs/design/rules/S7-company-of-an-unambiguous-credential.md b/docs/design/rules/S7-company-of-an-unambiguous-credential.md index 755c30312..1e2d92765 100644 --- a/docs/design/rules/S7-company-of-an-unambiguous-credential.md +++ b/docs/design/rules/S7-company-of-an-unambiguous-credential.md @@ -10,7 +10,7 @@ "doe, jane v phd do" → suffix="phd do" ``` - **A maiden marker's clause.** The company does not reach across a maiden marker's clause (M2), whose name words stand between. -- **What reports.** A member the company decides reports the fork as a counted pick does, wherever it stands: at the trailing slots S4 names, and in a part after a family comma that the company leaves holding no name word, which reads wholly as the credential run. A member its own capitals already made the credential reports nothing new. +- **What reports.** A member the company decides reports the fork as a counted pick does, wherever it stands: at the trailing slots S4 names, and in a part after a family comma that the company leaves holding no name word, which reads wholly as the credential run. A member its own capitals already made the credential reports nothing (S6). ``` "Smith, PhD Ma" → ambiguities=("suffix-or-name",) ``` diff --git a/docs/design/rules/S8-credentials-trailing-run.md b/docs/design/rules/S8-credentials-trailing-run.md index a668393ec..9f4ab8b7b 100644 --- a/docs/design/rules/S8-credentials-trailing-run.md +++ b/docs/design/rules/S8-credentials-trailing-run.md @@ -4,9 +4,11 @@ **Then:** a credential after the name core starts a run to the end of its part (#602): every later word of that part reads as a suffix, except a title word, which reads as a title. -- **Where the run starts.** An unambiguous suffix word of two or more letters, in any script, standing after two name words where no comma divides the name or before a suffix comma, or after the given word in the part after a family comma, makes every later word of that part a suffix. So does a roman numeral of two or more letters that reads as the generation (S10), whether the vocabulary lists it or its shape alone reads it. +- **Where the run starts.** An unambiguous suffix word of two or more letters, in any script, standing after two name words where no comma divides the name or before a suffix comma, or after the given word in the part after a family comma, makes every later word of that part a suffix. So does a roman numeral of two or more letters that reads as the generation (S10), whether the vocabulary lists it or its shape alone reads it, and so does a listed member of the ambiguous class its writing settles as the credential, written in capitals in a name of mixed case (S6). ``` "John Smith VI Jones" → suffix="VI Jones" + "John Smith MA Jones" → suffix="MA Jones" + "Smith, John MA Jones" → suffix="MA Jones" "Smith, John VI Jones" → suffix="VI Jones" "Smith, John PhD Jones" → suffix="PhD Jones" "John Smith PhD Prof. Ma" → suffix="PhD Ma" @@ -18,11 +20,14 @@ "John von PhD Jones" → family="von" "John von PhD Jones" → suffix="PhD Jones" ``` -- **What is a credential here.** A title word never starts the run, and neither does a member of the ambiguous class, a single letter, a connective, a word that is also particle or bound given-name vocabulary: a credential is the writer's mark that the post-nominals have begun, and those carry no such mark. An honorific or a generational word carries it as a credential does, whatever its script. +- **What is a credential here.** A title word never starts the run, and neither does a member of the ambiguous class its writing leaves unsettled, a single letter, a connective, a word that is also particle or bound given-name vocabulary, the capitals of `DO` included: a credential is the writer's mark that the post-nominals have begun, and those carry no such mark. An honorific or a generational word carries it as a credential does, whatever its script. ``` "Mary Jane King Smith" → family="Smith" "Josep Carod i Rovira" → family="Carod i Rovira" "Mohamed Ali Abd Allah" → family="Allah" + "John Smith Ma Jones" → family="Jones" + "JOHN SMITH MA JONES" → family="JONES" + "John Smith DO Jones" → family="DO Jones" "Antonio Pavlović mlađi Jones" → suffix="mlađi Jones" "Pieter Bruegel stariji Jones" → suffix="stariji Jones" "Ана Хорват старији Џонс" → suffix="старији Џонс" @@ -55,7 +60,15 @@ ``` "John Smith Pharm D Jones" → suffix="Pharm D Jones" ``` -- **An ambiguous acronym written with its periods** ("John Smith M.A. Jones"): writing the periods makes it unambiguously a credential (S3), so it is no member of the ambiguous class here and starts the run. +- **An ambiguous acronym written with its periods** ("John Smith M.A. Jones"): writing the periods makes it unambiguously a credential (S3), so it is no member of the ambiguous class here and starts the run, as its capitals in a name of mixed case make it start it (S6). + +**Accepted:** + +- A writer's own initials, written in capitals between the given names and the family name, that spell a listed member of the ambiguous class start the run as that credential does, and the family name behind them reads as a suffix, reported: the capitals are the credential's writing and the initials' alike, and the periods' spelling already read this way. + ``` + "Maria Ana MA Silva" → suffix="MA Silva" + "Maria Ana M.A. Silva" → suffix="M.A. Silva" + ``` --- **Signal:** neighbor: an unambiguous credential after the name core, and the rest of its part diff --git a/docs/design/vocabulary-model-rfc.md b/docs/design/vocabulary-model-rfc.md index d3ce85693..544eb6a7f 100644 --- a/docs/design/vocabulary-model-rfc.md +++ b/docs/design/vocabulary-model-rfc.md @@ -66,7 +66,7 @@ Every clash the parser decides is between readings of one token, and phase 2 put - **Declared name readings**, `given` and `family`, written by hand on a word some file claims or refuses (decision 11), with dated counts as provenance that never compute a reading (decision 12). Nobody maintains surname lists. - **Shape readings**, which today are rules standing around the vocabulary: the initial ([rules/T4](rules/T4-bare-letter-as-an-initial.md), a bare letter as an initial; [rules/T5](rules/T5-written-shape-of-an-initial.md), the written shape of an initial), the leading abbreviation title ([rules/H2](rules/H2-leading-abbreviation-title.md)), the dotted acronym ([rules/S3](rules/S3-credentials-written-in-joined-chunks.md)), the capitals acronym ([rules/S9](rules/S9-unlisted-words-that-join-the-ambiguous-class-by-shape.md), unlisted words that join the ambiguous class by shape), the roman numeral past the listed ones ([rules/S10](rules/S10-trailing-roman-numeral.md)), paired initials ([rules/C5](rules/C5-paired-initials-after-the-comma.md), paired initials after the comma), and the joined and split spellings of a credential that phase 2 adds. -Shape readings join the pool because of decision 20, which lets a pack's preferred reading outrank a shape-derived one, and which phase 2 applies in the default as well (an abbreviated given name over the leading-abbreviation title, below): a reading can outrank a shape-derived one only if the shape's answer is a reading the gate weighs, not a verdict already written into a tag. Settling a clash by rewriting a tag also erases the word's other readings: retagging a capitals `DO` as an unambiguous credential let its particle reading take it into the family (`Doe, John DO`), which is why the case cue for credentials (#785) waits for the pool. +Shape readings join the pool because of decision 20, which lets a pack's preferred reading outrank a shape-derived one, and which phase 2 applies in the default as well (an abbreviated given name over the leading-abbreviation title, below): a reading can outrank a shape-derived one only if the shape's answer is a reading the gate weighs, not a verdict already written into a tag. Settling a clash by rewriting a tag also erases the word's other readings: retagging a capitals `DO` as an unambiguous credential let its particle reading take it into the family (`Doe, John DO`), which is why the case cue for credentials (#785) waited for the pool. Phase 2g (2026-10-25) settles it where each site chooses a reading, the word keeping its tags ([rules/S6](rules/S6-case-signal-at-the-suffix-slot.md)). ### Ambiguity from readings diff --git a/docs/release_log.rst b/docs/release_log.rst index 458a74101..8fe6b2f4b 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -12,6 +12,7 @@ Release Log - **Read a credential written with a space inside it, such as Pharm D, as one credential.** ``John Smith, Pharm D`` gives first ``John``, last ``Smith``, suffix ``Pharm D``, where 1.4.0 through 2.6.0 gave first ``Pharm``, middle ``D``, last ``John Smith``. It counts only where the name has words to spare, so ``Smith, Pharm D`` still gives first ``Pharm``, and one opening with a single letter is still two words (``John Smith, M. D.`` still gives first ``M.``). :issue:`752` - **Fix a split Ph. D. in the middle of a name stopping short of the words after it.** ``John Smith Ph. D. Jones`` gives suffix ``Ph. D. Jones``, as ``John Smith PhD Jones`` does, where 1.4.0 through 2.6.0 gave last ``Jones``, suffix ``Ph. D.``; ``John Smith V Ph. D.`` gives last ``V``, as ``John Smith V PhD`` does, where 1.4.0 through 2.6.0 read ``V`` as a suffix. :issue:`827` - **Read the Brazilian agnomens and French fils as suffixes where the name has a word to spare.** ``Pedro Alves Filho`` and ``Alexandre Dumas fils`` give last ``Alves`` and ``Dumas``, suffix ``Filho`` and ``fils``, where 1.4.0 through 2.6.0 gave middle ``Alves`` and ``Dumas``, last ``Filho`` and ``fils``; ``Neto``, ``Netto``, ``Sobrinho`` and the feminines ``Filha``, ``Neta``, ``Sobrinha`` read the same way. Each is a surname too, so a name of two words keeps it as the last name (``Ana Neto``, ``PEDRO FILHO``) and ``Fils`` in Title case in a mixed-case name stays the last name (``Marie Fils``), the parse reports the choice as a ``suffix-or-name`` ambiguity. ``Lexicon.suffix_words_ambiguous`` lists the words and ``Lexicon.agnomens`` the ones read by the word count alone. :issue:`843`, :issue:`752` + - **Read a credential that is also a name, written in capitals in a mixed-case name, as settled.** ``John Smith MA`` reports no ``suffix-or-name`` ambiguity, where 2.0.0 through 2.6.0 reported one, and ``John Smith MA Jones`` gives suffix ``MA Jones``, where 1.4.0 through 2.6.0 gave middle ``Smith MA``, last ``Jones``, as ``John Smith M.A. Jones`` already read. A name written in one case still reports (``JOHN SMITH MA``), and initials written in capitals before the last name read the same way (``Maria Ana MA Silva`` gives suffix ``MA Silva``). :issue:`785`, :issue:`752` - **A Lexicon or Parser pickled by an earlier release no longer loads.** Loading one raises ``ValueError`` naming the new fields (``suffix_words_ambiguous``, ``agnomens``); re-pickle after upgrading. ``ParsedName`` and ``HumanName`` pickles still load. :issue:`752` * 2.6.0 - October 25, 2026 diff --git a/docs/usage.rst b/docs/usage.rst index 24a6638f7..9f1573da2 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1017,21 +1017,25 @@ the same way. Which reading a bare one gets depends on what the writing says: - **Capitals** in a name written in more than one case lean to the - credential, even with no words to spare (``Jack MA``). + credential, even with no words to spare (``Jack MA``). They settle it, + as periods do, so nothing is reported. - **Any other cased spelling** that is not wholly lowercase leans to the surname, even with words to spare (``Jack Ma``, ``John Smith Ma``). - **No signal** — all lowercase, or a name written wholly in one case — leaves it to the count: the credential with two or more words before it, not counting a title or a nickname, the surname otherwise. -Either way the choice is recorded: +Where the writing leaves the choice to the parser, the choice is +recorded: .. doctest:: - >>> parse("John Smith MA").suffix + >>> parse("JOHN SMITH MA").suffix 'MA' - >>> [a.kind.value for a in parse("John Smith MA").ambiguities] + >>> [a.kind.value for a in parse("JOHN SMITH MA").ambiguities] ['suffix-or-name'] + >>> parse("John Smith MA").ambiguities + () >>> parse("Jack MA").suffix 'MA' >>> parse("Jack Ma").family diff --git a/nameparser/_pipeline/_assign.py b/nameparser/_pipeline/_assign.py index 94a097897..e53bb112d 100644 --- a/nameparser/_pipeline/_assign.py +++ b/nameparser/_pipeline/_assign.py @@ -101,7 +101,8 @@ anchor_in_reach, credential_at_the_given_slot, given_slot_anchors, _NOT_A_RUN_START, has_name_content, is_lone_never_given_particle, Peel, TailRead, is_suffix_piece, is_title_piece, leading_titles, - particle_tail, listed_lean, peel_walk, starts_a_credential_run, + particle_tail, listed_lean, peel_walk, settled_by_case, + starts_a_credential_run, tail_reading, trailing_titles, ) from nameparser._pipeline._state import ( @@ -1372,6 +1373,7 @@ def reads_as_a_suffix(m: int, titled: frozenset[int]) -> bool: if (m not in titled_idx and ("suffix" in ptags[m] or "split" in ptags[m] or (("vocab:suffix" in tags + or AMBIGUOUS_ACRONYM_TAG in tags or _ROMAN.match(tokens[pieces[m][0]].text .rstrip(".")) is not None) and tags.isdisjoint(_NOT_A_RUN_START))) @@ -1455,12 +1457,15 @@ def reads_as_a_suffix(m: int, titled: frozenset[int]) -> bool: _AMBIGUOUS_CREDENTIAL_TAGS)): # a class member inside the run is still the # fork #531's report names, now read as the - # credential, as #544's anchor reads one - ambiguities.append(PendingAmbiguity( - AmbiguityKind.SUFFIX_OR_NAME, - f"{tokens[piece[0]].text!r} ending the given " - f"part is also an ordinary name word; read " - f"as a credential", (piece[0],))) + # credential, as #544's anchor reads one -- + # unless its capitals settled it (rules/S6) + if not settled_by_case(tokens[piece[0]], + state.one_case): + ambiguities.append(PendingAmbiguity( + AmbiguityKind.SUFFIX_OR_NAME, + f"{tokens[piece[0]].text!r} ending the given " + f"part is also an ordinary name word; read " + f"as a credential", (piece[0],))) # the run's own first word is the credential that # starts it, a split one ('Pharm D') included elif (not suffix_here and m != sticky_from @@ -1500,7 +1505,11 @@ def reads_as_a_suffix(m: int, titled: frozenset[int]) -> bool: and not tokens[piece[0]].tags.isdisjoint( _AMBIGUOUS_CREDENTIAL_TAGS) and (suffix_here - or "particle" not in tokens[piece[0]].tags)): + or "particle" not in tokens[piece[0]].tags) + # rules/S6: "a clash settled by the writing is + # not reported" + and not (suffix_here and settled_by_case( + tokens[piece[0]], state.one_case))): # The SAME floor the predicate measures members # against, so this report has no walk of its own to # regress: it had one, and on a run the predicate's diff --git a/nameparser/_pipeline/_comma.py b/nameparser/_pipeline/_comma.py index 6d3c25970..19af223d9 100644 --- a/nameparser/_pipeline/_comma.py +++ b/nameparser/_pipeline/_comma.py @@ -34,7 +34,7 @@ from nameparser._pipeline._pieces import ( split_credential_report, has_name_content, is_suffix_piece, is_title_piece, join_connectives, - listed_lean, own_words, segment_suffix_reading, + listed_lean, own_words, segment_suffix_reading, settled_by_case, ) from nameparser._pipeline._state import ( AMBIGUOUS_ACRONYM_TAG, AMBIGUOUS_WORD_TAG, SHAPE_ACRONYM_TAG, @@ -604,13 +604,19 @@ def decide(state: ParseState, splits: bool | None = True) -> ParseState: and AMBIGUOUS_ACRONYM_TAG in tokens[p[0]].tags] listed = [i for i in members if SHAPE_ACRONYM_TAG not in tokens[i].tags] - settled = (len(p1) > 1 and bool(members) + settled = (bool(members) and len(listed) == len(members) and all(listed_lean(tokens[i], state.one_case) == "credential" and (names >= 2 or AMBIGUOUS_WORD_TAG not in tokens[i].tags) - for i in listed)) + for i in listed) + # rules/S6: "a clash settled by the writing is not + # reported" -- a lone word only where it is an + # acronym its capitals settle + and (len(p1) > 1 + or settled_by_case(tokens[listed[0]], + state.one_case))) member_set = frozenset(members) pairs = [k for k, p in enumerate(p1) if p[0] in member_set and (is_paired_initials(texts[p[0]]) @@ -644,11 +650,14 @@ def decide(state: ParseState, splits: bool | None = True) -> ParseState: AmbiguityKind.SUFFIX_OR_NAME, f"{what}; the part before the comma holds {names} name words, " f"so it is read as a credential run", tuple(part))) - elif len(p1[0]) == 1 and p1[0][0] in listed: + elif (len(p1[0]) == 1 and p1[0][0] in listed + and not (settled and reading[0] + and settled_by_case(tokens[p1[0][0]], state.one_case))): # the first word after the comma reports as it always does, # naming the role bound above: the listing form binds a word # the reading declines as a title ('John, Si.', #793), and no - # later rule moves a word of the emptied part + # later rule moves a word of the emptied part; one its capitals + # settled as the credential does not i = p1[0][0] field = "a credential" if reading[0] else "a title" ambiguities.append(PendingAmbiguity( diff --git a/nameparser/_pipeline/_pieces.py b/nameparser/_pipeline/_pieces.py index 1ad8a7ae4..f0af83c7f 100644 --- a/nameparser/_pipeline/_pieces.py +++ b/nameparser/_pipeline/_pieces.py @@ -464,7 +464,8 @@ def is_suffix_piece(piece: Sequence[int], ptags: Set[str], # rules/S8: "A title word never starts the run, and neither does a -# member of the ambiguous class, a single letter, a connective" -- what +# member of the ambiguous class its writing leaves unsettled, a single +# letter, a connective" -- what # starts #602's credential run, one predicate for the trailing peel and # the given-part walk (mechanisms.md#ONE-PREDICATE-PER-QUESTION) def starts_a_credential_run(piece: Sequence[int], ptags: Set[str], @@ -517,6 +518,13 @@ def starts_a_credential_run(piece: Sequence[int], ptags: Set[str], stem = tok.text.rstrip(".") if len(stem) >= 2 and _ROMAN.match(stem) is not None: return reads_as_generation(tok.text, tok.tags, preceding, one_case) + # rules/S8: "and so does a listed member of the ambiguous class its + # writing settles as the credential" -- the capitals mark the + # post-nominals as periods do; the particle test above still + # refuses 'DO' + if AMBIGUOUS_ACRONYM_TAG in tok.tags: + return (settled_by_case(tok, one_case) + and sum(map(str.isalpha, tok.text)) >= 2) if not is_suffix_piece(piece, ptags, tokens): return False # C-level, no generator frame per character @@ -1081,6 +1089,22 @@ def listed_lean(token: WorkToken, one_case: bool | None) -> Lean | None: return ambiguous_lean(token.text, one_case) +# rules/S6: "a clash settled by the writing is not reported" -- the +# gate's form step for a listed acronym, asked where a reading is +# chosen, so the word keeps its tags and with them its other readings +# (history: decisions/S6 (#785)) +def settled_by_case(token: WorkToken, one_case: bool | None) -> bool: + """Whether the writing settles a LISTED ambiguous acronym as the + credential: written in capitals in a name of mixed case + (`listed_lean`). Only the clash between the credential and the + name reading is settled; the word stays a member of the class, so + a particle reading ('DO') or the place a maiden clause gives its + first word still answers as before. A suffix word that is also a + name ('fils') is S11's and is not asked here.""" + return (AMBIGUOUS_WORD_TAG not in token.tags + and listed_lean(token, one_case) == "credential") + + def credential_at_the_given_slot( token: WorkToken, one_case: bool | None, anchored: Callable[[], bool] | None = None) -> bool: @@ -1354,6 +1378,11 @@ def peel_trailing(rest: Sequence[int], pieces: Sequence[Sequence[int]], # then the floor, then the count-or-lean, in that order, with # nothing computed a step earlier could discard. if bare_ambiguous and k >= 2: + # rules/S6: "a clash settled by the writing is not + # reported" -- taken as any suffix word is, and no pick + if settled_by_case(tokens[piece[0]], one_case): + k -= 1 + continue picks.append(tuple(piece)) lean = listed_lean(tokens[piece[0]], one_case) # peeling still leaves given + family, or the writing says @@ -1471,6 +1500,7 @@ def run_start(rest: Sequence[int], names: int, tokens)): return p if (("vocab:suffix" in tags or "split" in ptags[q] + or AMBIGUOUS_ACRONYM_TAG in tags or _ROMAN.match(tokens[pieces[q][0]].text.rstrip(".")) is not None) and tags.isdisjoint(_NOT_A_RUN_START) @@ -1770,7 +1800,12 @@ def trailing_candidates(lo: int, pieces: Sequence[Sequence[int]], is not None) and starts_a_credential_run( pieces[j], ptags[j], tokens, - tokens[pieces[j - 1][0]].text)), + tokens[pieces[j - 1][0]].text) + # a member its capitals settle, asked with the case as + # mixed so the superset holds whatever the name's case + or AMBIGUOUS_ACRONYM_TAG in tokens[pieces[j][0]].tags + and starts_a_credential_run(pieces[j], ptags[j], tokens, + None, False)), c) diff --git a/tests/v2/cases.py b/tests/v2/cases.py index f7aaa050f..1319edf42 100644 --- a/tests/v2/cases.py +++ b/tests/v2/cases.py @@ -633,7 +633,7 @@ def _check_cjk_shape_purity(self) -> None: Case("ambiguous_surname_acronyms", "Jack MA", {"given": "Jack", "suffix": "MA"}, classification="fix(#289)", - ambiguities=("suffix-or-name", "given-or-family"), + ambiguities=("given-or-family",), notes="the defect #289 reports: an all-caps member of the " "ambiguous set inside a mixed-case name is written the " "way a credential is written, so it is taken as one " @@ -649,7 +649,7 @@ def _check_cjk_shape_purity(self) -> None: Case("ambiguous_surname_acronym_with_suffix", "Jack MA Jr", {"given": "Jack", "suffix": "MA Jr"}, classification="fix(#289)", - ambiguities=("suffix-or-name", "given-or-family"), + ambiguities=("given-or-family",), notes="'Jr' peels first as unambiguous vocabulary; 'MA' is " "then the last piece, and the caps lean takes it with " "no words to spare rather than leaving it the family " @@ -658,7 +658,6 @@ def _check_cjk_shape_purity(self) -> None: Case("ambiguous_acronym_is_a_suffix_when_a_family_name_remains", "John Smith MA", {"given": "John", "family": "Smith", "suffix": "MA"}, - ambiguities=("suffix-or-name",), notes="the other half of the same rule: three pieces means " "peeling 'MA' still leaves given+family, so the " "credential reading wins (v1 parity)"), @@ -775,7 +774,6 @@ def _check_cjk_shape_purity(self) -> None: Case("comma_ambiguous_acronym_ba", "Smith, BA", {"family": "Smith", "suffix": "BA"}, classification="fix(#289)", - ambiguities=("suffix-or-name",), notes="the marking's comma cost, and it is 'Smith, Ed' " "arriving for a second word: with 'ba' ambiguous, S4 " "declines the post-comma credential and C4 reads it as " @@ -785,10 +783,10 @@ def _check_cjk_shape_purity(self) -> None: "way a business form is written"), Case("ba_is_a_suffix_when_a_family_name_remains", "John Smith BA", {"given": "John", "family": "Smith", "suffix": "BA"}, - ambiguities=("suffix-or-name",), - notes="the words-to-spare half: a full name in front leaves " - "the credential reading available, so the role does " - "NOT move -- only the flag is new. The twin of " + notes="the words-to-spare half: a full name in front leaves the " + "credential reading available, so the role does NOT move; " + "the flag the fork carried went with #785, the capitals " + "settling the word (rules/S6). The twin of " "ambiguous_acronym_is_a_suffix_when_a_family_name_remains " "above"), # -- #791: 'si' and 'chi' take the marking 'ba' took. Both are common @@ -833,17 +831,16 @@ def _check_cjk_shape_purity(self) -> None: Case("capital_si_is_a_suffix_when_a_family_name_remains", "John Smith SI", {"given": "John", "family": "Smith", "suffix": "SI"}, - ambiguities=("suffix-or-name",), notes="the words-to-spare half, and the capitals lean: SI is " "written in capitals inside a mixed-case name, so the " - "credential reading is the stronger one and the role " - "does NOT move, only the flag is new. 1.4.0 read the " - "same suffix"), + "credential reading is the stronger one and the role does " + "NOT move; the flag it carried went with #785, the capitals " + "settling the word (rules/S6). 1.4.0 read the same suffix"), Case("comma_capital_si_is_a_suffix", "John Smith, SI", {"given": "John", "family": "Smith", "suffix": "SI"}, - ambiguities=("suffix-or-name",), - notes="the comma spelling of the row above, which 1.4.0 read " - "as suffix 'SI' too; only the flag is new"), + notes="the comma spelling of the row above, which 1.4.0 read as " + "suffix 'SI' too; it reports nothing since #785, the " + "capitals settling it (rules/S6)"), Case("dotted_si_is_a_suffix_without_a_flag", "John Smith S.I.", {"given": "John", "family": "Smith", "suffix": "S.I."}, notes="the dotted shape (rules/S3) is the one spelling the " @@ -894,12 +891,12 @@ def _check_cjk_shape_purity(self) -> None: "family name"), Case("all_caps_capa_is_still_a_credential", "John Smith CAPA", {"given": "John", "family": "Smith", "suffix": "CAPA"}, - ambiguities=("suffix-or-name",), classification="fix(#796)", notes="the contrasting spelling: an all-caps member of the " "ambiguous set in a name with words to spare keeps its " - "credential reading (rules/S6), so the fork is pinned " - "in both directions"), + "credential reading (rules/S6), so the reading is pinned in " + "both directions; since #785 it reports nothing, the " + "capitals settling the word"), # -- #797: credentials the comma path read as the given name because # no list held them (NPPES and ClinicalTrials.gov counts, in the # entries' notes). Listed, they end the name; a bare ambiguous one @@ -925,12 +922,15 @@ def _check_cjk_shape_purity(self) -> None: {"suffix": "FNP-C"}, classification="fix(#797)", notes="the empty-head comma path, with a hyphen-joined word"), - Case("comma_pa_keeps_its_suffix_and_reports_the_fork", "John Smith, PA", - {"given": "John", "family": "Smith", "suffix": "PA"}, - ambiguities=("suffix-or-name",), + Case("comma_pa_keeps_its_suffix_and_reports_the_fork", "JOHN SMITH, PA", + {"given": "JOHN", "family": "SMITH", "suffix": "PA"}, classification="fix(#797)", + ambiguities=("suffix-or-name",), notes="PA is ambiguous (Census 739 family, SSA 1,343 given), so " - "the credential reading is reported as a fork"), + "the credential reading is reported as a fork. Written in " + "one case since #785: 'John Smith, PA' reads the same " + "suffix in silence, its capitals settling the word " + "(rules/S6)"), Case("john_pa_is_a_family_name", "John Pa", {"given": "John", "family": "Pa"}, ambiguities=("suffix-or-name",), @@ -979,7 +979,6 @@ def _check_cjk_shape_purity(self) -> None: "reporting the fork"), Case("one_word_head_capitals_ing_is_a_credential", "Novak, ING.", {"family": "Novak", "suffix": "ING."}, - ambiguities=("suffix-or-name",), classification="fix(#848)", notes="the capitals lean (rules/S6): in a name not written " "wholly in capitals, ING. reads as MA. and PA. do, the " @@ -1011,7 +1010,7 @@ def _check_cjk_shape_purity(self) -> None: "(nickname 'Bob', first 'Si')"), Case("capital_si_after_a_surname_is_a_suffix", "Luo SI", {"given": "Luo", "suffix": "SI"}, - ambiguities=("given-or-family", "suffix-or-name"), + ambiguities=("given-or-family",), classification="fix(#289)", notes="the caps lean (decisions/S6, #289): all-capitals SI is " "the credential even as the only word after a name, so " @@ -1019,7 +1018,6 @@ def _check_cjk_shape_purity(self) -> None: "does not reach it. 1.4.0 read family 'SI'"), Case("comma_capital_si_after_a_surname_is_a_suffix", "Wang, SI", {"family": "Wang", "suffix": "SI"}, - ambiguities=("suffix-or-name",), classification="fix(#289)", notes="the comma form of the caps lean, as 'Smith, BA' reads " "(#289). 1.4.0 read given 'SI' and family 'Wang'"), @@ -1094,7 +1092,6 @@ def _check_cjk_shape_purity(self) -> None: "position decides, as at 1.4.0"), Case("title_head_capital_si_is_a_suffix", "Dr. Luo SI", {"title": "Dr.", "family": "Luo", "suffix": "SI"}, - ambiguities=("suffix-or-name",), classification="fix(#289)", notes="capital_si_after_a_surname_is_a_suffix behind a title: " "the caps lean takes SI, and H1 makes the one name word " @@ -2065,15 +2062,14 @@ def _check_cjk_shape_purity(self) -> None: {"title": "Prof.", "given": "Berg", "family": "de", "suffix": "MA"}, classification="fix(comma-family)", - ambiguities=("suffix-or-name",), - notes="#573's boundary: a comma followed by no name word fixed " - "no family, so the part before it reads positionally, " - "its trailing credential included. A first draft of " - "#573 skipped the chain's trailing stop for the whole " - "family part and read family 'de MA'. The row pins the " - "suffix and its report; the all-particle family 'de' " - "is the positional read's, older than #573, and not " - "endorsed here"), + notes="#573's boundary: a comma followed by no name word fixed no " + "family, so the part before it reads positionally, its " + "trailing credential included. A first draft of #573 " + "skipped the chain's trailing stop for the whole family " + "part and read family 'de MA'. The row pins the suffix " + "(reported until #785, whose capitals settle it now, " + "rules/S6); the all-particle family 'de' is the positional " + "read's, older than #573, and not endorsed here"), Case("a_particle_suffix_word_between_credentials_stays_in_the_run", "DOE, JANE PHD VD MA", {"given": "JANE", "family": "DOE", "suffix": "PHD VD MA"}, @@ -2106,7 +2102,7 @@ def _check_cjk_shape_purity(self) -> None: "Doe, Jane PhD do MA", {"given": "Jane", "family": "Doe", "suffix": "PhD do MA"}, classification="fix(#436/#437)", - ambiguities=("suffix-or-name", "suffix-or-name"), + ambiguities=("suffix-or-name",), notes="#573's boundary: 'do' is a member assign already read " "as the credential, so P6 finds no particle run to " "attach and the third exception has nothing to " @@ -2144,7 +2140,6 @@ def _check_cjk_shape_purity(self) -> None: "Smith, de MA", {"family": "Smith de", "suffix": "MA"}, classification="fix(comma-precomma-family)", - ambiguities=("suffix-or-name",), notes="S6: in a mixed-case name the capitals speak even with " "no words to spare, so the never-given lead does not " "take 'MA' -- 'Smith, de Ma' does take it"), @@ -2181,7 +2176,6 @@ def _check_cjk_shape_purity(self) -> None: "van der Berg, MA", {"family": "van der Berg", "suffix": "MA"}, classification="fix(#575)", - ambiguities=("suffix-or-name",), notes="one name word before the comma, so C4 reads the case " "and capitals in a mixed-case name make 'MA' the " "credential, as 'Berg, MA' reads. The count had split " @@ -2394,7 +2388,7 @@ def _check_cjk_shape_purity(self) -> None: "Doe, Jane nee Smith MA Jr Ed", {"given": "Jane", "middle": "nee Smith", "family": "Doe", "suffix": "MA Jr Ed"}, classification="fix(#601)", - ambiguities=("suffix-or-name", "suffix-or-name"), + ambiguities=("suffix-or-name",), notes="#601: a marker in the given part after a family comma is an " "ordinary word (rules/M2). Before #601: given 'Jane', family " "'Doe', suffix 'MA Jr Ed', maiden 'Smith'. As pinned then: the " @@ -2510,8 +2504,7 @@ def _check_cjk_shape_purity(self) -> None: "parity, the flag is #449's"), Case("ambiguous_acronym_suffix_with_middle", "John Q Smith MA", {"given": "John", "middle": "Q", "family": "Smith", - "suffix": "MA"}, - ambiguities=("suffix-or-name",)), + "suffix": "MA"}), # #604: the one-letter particle against the initial it spells # (rules/P7). The period decides, in every position a particle # reading could otherwise claim the letter. @@ -3393,7 +3386,7 @@ def _check_cjk_shape_purity(self) -> None: Case("caps_ambiguous_leans_credential", "Jack MA", {"given": "Jack", "suffix": "MA"}, classification="fix(#289)", - ambiguities=("suffix-or-name", "given-or-family"), + ambiguities=("given-or-family",), notes="the defect. An all-caps member of the ambiguous set " "inside a mixed-case name is written the way a " "credential is written, so it is taken as one even " @@ -3405,7 +3398,7 @@ def _check_cjk_shape_purity(self) -> None: Case("caps_ambiguous_leans_credential_with_a_period", "Jack MA.", {"given": "Jack", "suffix": "MA."}, classification="fix(#289)", - ambiguities=("suffix-or-name", "given-or-family"), + ambiguities=("given-or-family",), notes="the lean reads CASE, not periods: a single trailing " "period is the abbreviation shape any word can wear " "(rules/S2) and changes nothing here, where the " @@ -3457,7 +3450,6 @@ def _check_cjk_shape_purity(self) -> None: Case("mixed_case_caps_ambiguous_with_words_to_spare", "john smith MA", {"given": "john", "family": "smith", "suffix": "MA"}, - ambiguities=("suffix-or-name",), notes="the lean and the count AGREE here, which is why the " "row does not move: capitals against lowercase lean " "credential, and three pieces read credential anyway", @@ -3490,17 +3482,16 @@ def _check_cjk_shape_purity(self) -> None: Case("the_lean_reaches_the_post_comma_slot", "Smith, MA", {"family": "Smith", "suffix": "MA"}, classification="fix(#289)", - ambiguities=("suffix-or-name",), - notes="Derek's own reading of #289: positive evidence " - "outranks position, so the credential lean fires with " - "ONE word before the comma where the words-to-spare " - "count would not. The first report of the comma's OWN " - "decision rides with it -- C2's structural flag already " - "reports on the comma path, but reports what the parse " - "could not recognize, not a fork it called " - "(rules/C11's report, scoped to this class; P6's " - "attachment fork has reported on a family-comma path " - "since 2.3, e.g. 'Berg, Jan vd')", + notes="Derek's own reading of #289: positive evidence outranks " + "position, so the credential lean fires with ONE word " + "before the comma where the words-to-spare count would not. " + "The first report of the comma's OWN decision rode with it " + "until #785, which settles the capitals and drops it " + "(rules/S6) -- C2's structural flag already reports on the " + "comma path, but reports what the parse could not " + "recognize, not a fork it called (rules/C11's report, " + "scoped to this class; P6's attachment fork has reported on " + "a family-comma path since 2.3, e.g. 'Berg, Jan vd')", shape=2), Case("the_surname_lean_keeps_the_post_comma_given", "Smith, Ma", {"given": "Ma", "family": "Smith"}, @@ -3513,7 +3504,6 @@ def _check_cjk_shape_purity(self) -> None: Case("the_comma_count_is_of_name_words", "Smith Jr., MA", {"family": "Smith", "suffix": "Jr., MA"}, classification="fix(#289)", - ambiguities=("suffix-or-name",), notes="rules/C4's count for this class is of NAME words, " "not of words: two tokens and one name here, so the " "structure does not flip and the family survives. " @@ -3545,7 +3535,6 @@ def _check_cjk_shape_purity(self) -> None: "John Smith, MA", {"given": "John", "family": "Smith", "suffix": "MA"}, classification="fix(#289)", - ambiguities=("suffix-or-name",), notes="1.4.0 RESTORED: v1 read first John, last Smith, " "suffix MA and 2.0 through 2.3 read given 'MA' with " "the whole name in `family`. The comma structure moves " @@ -3644,15 +3633,14 @@ def _check_cjk_shape_purity(self) -> None: Case("two_different_forks_each_report_once", "Smith MA, Ed", {"given": "Smith", "suffix": "MA, Ed"}, classification="fix(#289)", - ambiguities=("suffix-or-name", "suffix-or-name", - "given-or-family"), + ambiguities=("suffix-or-name", "given-or-family"), notes="'Smith MA' reads two name words, flipping the comma " - "structure so 'Ed' joins the credential run; peeling " - "'Smith MA' positionally then peels 'MA' too (caps " - "lean, no words to spare), leaving 'Smith' the only " - "name word, which is also what turns on " - "GIVEN_OR_FAMILY. Three reports, none of them the same " - "fork reporting twice"), + "structure so 'Ed' joins the credential run; peeling 'Smith " + "MA' positionally then peels 'MA' too (caps lean, no words " + "to spare), leaving 'Smith' the only name word, which is " + "also what turns on GIVEN_OR_FAMILY. Two reports since " + "#785, the settled 'MA' reporting nothing (rules/S6), and " + "none of them the same fork reporting twice"), Case("a_caseless_script_wrote_no_contrast", "毛泽东, MA", {"given": "MA", "family": "毛泽东"}, classification="fix(#289)", @@ -3823,15 +3811,28 @@ def _check_cjk_shape_purity(self) -> None: "pair joins the run as any dotted-shape word does, the " "reading the switch off gives too"), Case("paired_initials_behind_an_ambiguous_credential_end_the_name", - "Jane Doe MA R.T.", - {"given": "Jane", "middle": "Doe MA", "family": "R.T."}, + "Jane Doe Ma R.T.", + {"given": "Jane", "middle": "Doe Ma", "family": "R.T."}, ambiguities=("suffix-or-name",), classification="fix(#864)", notes="rules/S3's Accepted cost: only an unambiguous suffix " - "word in front makes the pair the run's, so behind the " - "ambiguous MA the pair ends the name and takes MA in. " - "1.4.0 through 2.3.0 read the same; 2.4.0 through 2.5.0 " - "suffix 'MA R.T.'"), + "word in front, or an ambiguous one its capitals settle, " + "makes the pair the run's, so behind the Title-case Ma " + "the pair ends the name and takes Ma in. Until #785 the " + "row was written 'Jane Doe MA R.T.', which reads the " + "capitals' run now (the row below). 1.4.0 read the " + "same"), + Case("a_capitals_credential_in_front_of_paired_initials_starts_the_run", + "Jane Doe MA R.T.", + {"given": "Jane", "family": "Doe", "suffix": "MA R.T."}, + ambiguities=("suffix-or-name",), + classification="fix(#785)", + notes="S8: written in capitals in a name of mixed case, " + "MA is settled as the credential (S6) and starts the run " + "as PhD does ('Jane Doe PhD R.T.'), which takes the pair; " + "the report is the pair's, MA's own fork being settled. " + "1.4.0 through 2.3.0 and 2.6.0 read middle 'Doe MA', " + "family 'R.T.'; 2.4.0 through 2.5.0 suffix 'MA R.T.'"), Case("a_listed_two_letter_credential_keeps_the_suffix", "Hari Bahadur D.C.", {"given": "Hari", "family": "Bahadur", "suffix": "D.C."}, @@ -4350,7 +4351,6 @@ def _check_cjk_shape_purity(self) -> None: "Kim김민준씨, MA", {"family": "Kim김민준", "suffix": "씨, MA"}, classification="fix(#289)", - ambiguities=("suffix-or-name",), notes="script_segment asked `is_wholly_suffix` of the " "post-comma run WITHOUT the case fact segment had just " "recorded, so 'MA' read as name material, the run was " @@ -4365,7 +4365,6 @@ def _check_cjk_shape_purity(self) -> None: "Jo김민준씨, DO", {"family": "Jo김민준", "suffix": "씨, DO"}, classification="fix(#289)", - ambiguities=("suffix-or-name",), notes="a second member of the ambiguous set behind the same " "comma, so the row above pins the mechanism rather " "than the word 'MA' " @@ -4379,7 +4378,6 @@ def _check_cjk_shape_purity(self) -> None: {"title": "Dr.", "given": "민준", "family": "김", "suffix": "씨, MA"}, classification="fix(#289)", - ambiguities=("suffix-or-name",), notes="since #613's /simplify the comma's own decision divides " "the name too, reading a suffix comma behind the title " "and the name word (b547c247: family '김민준', the " @@ -4764,13 +4762,12 @@ def _check_cjk_shape_purity(self) -> None: Case("caps_switch_does_not_silence_the_listed_lean", "Jack MA", {"given": "Jack", "suffix": "MA"}, policy=Policy(unlisted_caps_suffixes=CapsSuffixes.EVERYWHERE), - ambiguities=("suffix-or-name", "given-or-family"), + ambiguities=("given-or-family",), notes="the switch must not touch a LISTED member's own " "#289 lean: identical to the default reading"), Case("caps_switch_does_not_silence_the_comma_lean", "Smith, MA", {"family": "Smith", "suffix": "MA"}, policy=Policy(unlisted_caps_suffixes=CapsSuffixes.EVERYWHERE), - ambiguities=("suffix-or-name",), notes="the comma-form twin of the row above, same guarantee"), # #516 review round: UNLISTED means in no wordlist at all, not # merely "no whole-token suffix vocabulary" -- a capitalized @@ -5190,32 +5187,34 @@ def _check_cjk_shape_purity(self) -> None: "whole pre-comma run is the family. The widest of the " "three silent moves"), Case("the_comma_flip_is_read_under_the_declared_order", - "John Smith, MA", - {"given": "Smith", "family": "John", "suffix": "MA"}, + "JOHN SMITH, MA", + {"given": "SMITH", "family": "JOHN", "suffix": "MA"}, policy=Policy(name_order=FAMILY_FIRST), classification="fix(#289)", ambiguities=("suffix-or-name",), notes="MEASURED. The count flips the structure whatever the " "order -- it counts NAME words, which no order changes " "-- and the pre-comma run is then read positionally, " - "so the declared order decides which of 'John Smith' " + "so the declared order decides which of 'JOHN SMITH' " "is the family. The report's wording quotes the count, " - "not a role, so it reads the same under every order"), + "not a role, so it reads the same under every order. " + "Written in one case since #785: in a name of mixed " + "case the capitals settle 'MA' and the flip reports " + "nothing (rules/S6)"), Case("two_caps_credentials_peel_as_a_run", "John MA XYZ", {"given": "John", "suffix": "MA XYZ"}, policy=Policy(unlisted_caps_suffixes=CapsSuffixes.EVERYWHERE), classification="fix(#516)", - ambiguities=("given-or-family", "suffix-or-name", - "suffix-or-name"), - notes="MEASURED, and the widest reading the switch reaches: " - "the peel walks from the END, so 'XYZ' goes first (two " - "pieces still stand behind it), and 'MA' then has " - "'John' alone behind it -- but its LISTED lean says " - "credential, which needs no words to spare. The name " - "loses its family name entirely and says so three " - "times. At the default it reads given John, middle MA, " - "family XYZ, silently"), + ambiguities=("given-or-family", "suffix-or-name"), + notes="MEASURED, and the widest reading the switch reaches: the " + "peel walks from the END, so 'XYZ' goes first (two pieces " + "still stand behind it), and 'MA' then has 'John' alone " + "behind it -- but its LISTED lean says credential, which " + "needs no words to spare. The name loses its family name " + "entirely and says so twice, the listed 'MA' settled by its " + "capitals and silent since #785 (rules/S6). At the default " + "it reads given John, middle MA, family XYZ, silently"), Case("the_caps_shape_is_script_agnostic_cyrillic", "Иван Петр ИВАНОВ", {"given": "Иван", "family": "Петр", "suffix": "ИВАНОВ"}, @@ -5794,7 +5793,6 @@ def _check_cjk_shape_purity(self) -> None: "Doe, John MA", {"given": "John", "family": "Doe", "suffix": "MA"}, classification="fix(#531)", - ambiguities=("suffix-or-name",), notes="1.4.0 RESTORED: v1 read suffix 'MA' and 2.0 through " "2.3 read middle 'MA' in silence. The comma fixed the " "family and the first post-comma word is the given " @@ -5841,7 +5839,6 @@ def _check_cjk_shape_purity(self) -> None: "Doe, John BA", {"given": "John", "family": "Doe", "suffix": "BA"}, classification="fix(#531)", - ambiguities=("suffix-or-name",), notes="'ba' is another listed member (one of those " "decisions/suffix-acronym-collisions marked " "ambiguous rather than removing), so the slot is not a " @@ -5852,7 +5849,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "John", "middle": "Q.", "family": "Doe", "suffix": "MA"}, classification="fix(#531)", - ambiguities=("suffix-or-name",), notes="the member leaves the given part and 'Q.' stays the " "middle initial it always was -- the slot reaches the " "trailing word, not the run in front of it. 1.4.0 " @@ -5862,21 +5858,19 @@ def _check_cjk_shape_purity(self) -> None: "Doe, John MA PhD", {"given": "John", "family": "Doe", "suffix": "MA PhD"}, classification="fix(#531)", - ambiguities=("suffix-or-name",), notes="'ending the given part' REACHES PAST the credentials " - "behind it, so 'MA' joins the run rather than being " - "walled off by it. Rendered with a SPACE and not a " - "comma: R1 derives suffix entries from the commas the " - "WRITER typed (#436/#437), so only 'Doe, John MA, PhD' " - "renders 'MA, PhD'. 1.4.0 wrote a comma in both. ONE " - "report: 'PhD' is settled vocabulary and carries " - "neither class tag", + "behind it, so 'MA' joins the run rather than being walled " + "off by it. Rendered with a SPACE and not a comma: R1 " + "derives suffix entries from the commas the WRITER typed " + "(#436/#437), so only 'Doe, John MA, PhD' renders 'MA, " + "PhD'. 1.4.0 wrote a comma in both. No report since #785: " + "the capitals settle 'MA' (rules/S6), and 'PhD' is settled " + "vocabulary carrying neither class tag", shape=2), Case("the_trailing_slot_reaches_past_a_generational_suffix", "Doe, John MA Jr", {"given": "John", "family": "Doe", "suffix": "MA Jr"}, classification="fix(#531)", - ambiguities=("suffix-or-name",), notes="the run behind the member does not have to be " "credentials -- a generational suffix is a suffix " "piece and the walk reaches past it the same way", @@ -5885,7 +5879,6 @@ def _check_cjk_shape_purity(self) -> None: "Doe, John PhD MA", {"given": "John", "family": "Doe", "suffix": "PhD MA"}, classification="fix(#531)", - ambiguities=("suffix-or-name",), notes="the member stands LAST here and the settled " "credential in front of it is what used to strand it: " "2.0 through 2.3 read middle 'MA' with suffix 'PhD', " @@ -5896,7 +5889,6 @@ def _check_cjk_shape_purity(self) -> None: "Doe, John MA, PhD", {"given": "John", "family": "Doe", "suffix": "MA, PhD"}, classification="fix(#531)", - ambiguities=("suffix-or-name",), notes="#144's two-segment restriction is NOT inherited by " "this branch. That restriction exists because a " "trailing 'V' before a third comma part is likely a " @@ -5907,14 +5899,16 @@ def _check_cjk_shape_purity(self) -> None: "row shows 'MA, PhD' where its spaced twin shows " "'MA PhD' (#436/#437)"), Case("two_members_in_the_trailing_run_report_twice", - "Doe, John MA JD", - {"given": "John", "family": "Doe", "suffix": "MA JD"}, + "DOE, JOHN MA JD", + {"given": "JOHN", "family": "DOE", "suffix": "MA JD"}, classification="fix(#531)", ambiguities=("suffix-or-name", "suffix-or-name"), notes="one decision, one report; two members, two reports -- " - "matching 'John Doe MA JD', which reports twice today. " + "matching 'JOHN DOE MA JD', which reports twice. " "No token is ever reported twice: these are two " - "tokens", + "tokens. Written in one case since #785: in a name of " + "mixed case the capitals settle both and neither " + "reports (rules/S6)", shape=2), # ---- the declined direction: the reading does NOT move and the # report is new. Derek's Q4, answered 'report both directions': @@ -5958,57 +5952,73 @@ def _check_cjk_shape_purity(self) -> None: {"given": "John", "middle": "Ma", "family": "Doe", "suffix": "JD"}, classification="fix(#531)", - ambiguities=("suffix-or-name", "suffix-or-name"), - notes="two members, two decisions, two reports -- and they " - "go opposite ways. 'JD' is all-caps and takes the " - "credential; 'Ma' is Title-cased and stays a name, " - "which also ENDS the run, so nothing in front of it is " - "reached"), + ambiguities=("suffix-or-name",), + notes="two members, two decisions, and they go opposite ways. " + "'JD' is all-caps and takes the credential, its capitals " + "settling it in silence since #785 (rules/S6); 'Ma' is " + "Title-cased and stays a name, which also ENDS the run, so " + "nothing in front of it is reached"), Case("a_declined_member_ends_the_trailing_run", - "Doe, John MA Ma", - {"given": "John", "middle": "MA Ma", "family": "Doe"}, + "Doe, John ma Ma", + {"given": "John", "middle": "ma Ma", "family": "Doe"}, classification="fix(#531)", ambiguities=("suffix-or-name",), notes="the trailing member declines on its Title case, and " "the walk stops at the declined pick rather than " - "continuing past it -- so the all-caps 'MA' in front " + "continuing past it -- so the lower-case 'ma' in front " "is never asked and never reports. ONE report, for " "'Ma'. Identical in shape to rules/S7's accepted " - "'Jack Wei Ma' clause", + "'Jack Wei Ma' clause. Until #785 the row was written " + "'Doe, John MA Ma', whose capitals now start the " + "credential's run (the row below). 1.4.0 read suffix " + "'ma, Ma'", + shape=2), + Case("a_capitals_member_in_the_given_part_starts_the_run", + "Doe, John MA Ma", + {"given": "John", "family": "Doe", "suffix": "MA Ma"}, + classification="fix(#785)", + ambiguities=("suffix-or-name",), + notes="S8: after a family comma a member its capitals " + "settle starts the credential's run behind the given " + "word, as 'Doe, John PhD Ma' does, and the run takes the " + "Title-case Ma, reported as the member it is. 1.4.0 read " + "suffix 'MA, Ma'; 2.0.0 through 2.6.0 middle 'MA Ma'", shape=2), Case("a_taken_member_behind_a_declined_one_still_reports", "Doe, John Ma MA", {"given": "John", "middle": "Ma", "family": "Doe", "suffix": "MA"}, classification="fix(#531)", - ambiguities=("suffix-or-name", "suffix-or-name"), + ambiguities=("suffix-or-name",), notes="the mirror of the row above, and the pair is the " "argument: order in the string decides which member " "the run reaches. Here the trailing 'MA' is taken, the " "run then reaches 'Ma', which declines and ends it"), Case("a_third_member_behind_the_declined_one_stays_silent", - "Doe, John MA Ma MA", - {"given": "John", "middle": "MA Ma", "family": "Doe", - "suffix": "MA"}, + "Doe, John ma Ma ma", + {"given": "John", "middle": "ma Ma", "family": "Doe", + "suffix": "ma"}, classification="fix(#531)", ambiguities=("suffix-or-name", "suffix-or-name"), notes="the run walked to its floor and then measured against " - "it: the trailing 'MA' is taken, 'Ma' declines on its " + "it: the trailing 'ma' is taken, 'Ma' declines on its " "Title case and STOPS the run there, and the leading " - "'MA' -- behind a piece this walk refused -- is an " + "'ma' -- behind a piece this walk refused -- is an " "ordinary middle name, silent. TWO reports, for 'Ma' " - "and the trailing 'MA'; the third member reaches no " + "and the trailing 'ma'; the third member reaches no " "fork. The row that would catch a floor left over from " "an earlier member, since the three members ask the " "same walk three times and only the first two are " - "above the floor. Today middle 'MA Ma MA', silent"), + "above the floor. Written in lower case, which no " + "writing settles, since #785: in capitals the members " + "settle and start the credential's run (rules/S6, S8). " + "1.4.0 read suffix 'ma, Ma, ma'"), # ---- H5: a trailing title is transparent to this reading ------ Case("a_trailing_title_is_transparent_to_the_slot", "Doe, John MA Prof.", {"title": "Prof.", "given": "John", "family": "Doe", "suffix": "MA"}, classification="fix(#531)", - ambiguities=("suffix-or-name",), notes="the walk starts at previous_kept(), which is the H5 " "chain's own notion of where the name ends -- so " "'past a trailing title' costs no second definition " @@ -6020,7 +6030,6 @@ def _check_cjk_shape_purity(self) -> None: {"title": "Prof.", "given": "John", "family": "Doe", "suffix": "MA"}, classification="fix(#531)", - ambiguities=("suffix-or-name",), notes="MEASURED, and a SECOND field moves: today this is " "middle 'Prof. MA'. Once 'MA' leaves `walkable` the " "H5 trailing-title chain reaches 'Prof.' and takes " @@ -6032,7 +6041,6 @@ def _check_cjk_shape_purity(self) -> None: {"title": "Dr.", "given": "John", "family": "Doe", "suffix": "MA"}, classification="fix(#531)", - ambiguities=("suffix-or-name",), notes="the leading title run is peeled before the walk " "starts, so the slot sees exactly what it sees " "without it. 1.4.0 parity", @@ -6041,7 +6049,6 @@ def _check_cjk_shape_purity(self) -> None: "Doe, J. MA", {"given": "J.", "family": "Doe", "suffix": "MA"}, classification="fix(#531)", - ambiguities=("suffix-or-name",), notes="the first post-comma piece is the given name " "whatever its shape, so a one-letter given leaves the " "member at the trailing slot as any other would. " @@ -6062,27 +6069,36 @@ def _check_cjk_shape_purity(self) -> None: # ---- the negative controls: the reading does NOT move and # NOTHING is reported ----------------------------------------- Case("a_name_word_behind_the_member_ends_the_reach", - "Doe, John MA Smith", - {"given": "John", "middle": "MA Smith", "family": "Doe"}, + "DOE, JOHN MA SMITH", + {"given": "JOHN", "middle": "MA SMITH", "family": "DOE"}, classification="fix(comma-family)", - notes="UNCHANGED and SILENT, and the silence is the point: " + notes="SILENT, and the silence is the point: " "a name word behind the member ends the trailing run, " "so the member is an ordinary middle name and no fork " "was consulted. AGENTS.md's 'a kind is worth adding " "only if a reader would hesitate too' is why this must " "stay silent rather than why it happens to -- that " "sentence is the 2.0-conventions section's, and this " - "note cited rules/A1 for it until 2026-09-19. NOT " - "1.4.0 " - "parity, and not #531's doing either: v1 read middle " - "'Smith', suffix 'MA', and 2.0.0 already read what " - "this row reads (both measured on the released wheels, " - "2026-09-18). What moved was 2.0 reading the " - "post-comma part's suffix vocabulary by POSITION, so " + "note cited rules/A1 for it until 2026-09-19. Written " + "in one case since #785: in a name of mixed case the " + "capitals settle the member, which then starts the " + "credential's run (the row below). NOT 1.4.0 parity: v1 " + "read middle 'SMITH', suffix 'MA', and 2.0.0 already " + "read what this row reads. What moved was 2.0 reading " + "the post-comma part's suffix vocabulary by POSITION, so " "the divergence files under the family-comma group " - "rather than under the case signal -- measured " - "case-independent, 'DOE, JOHN MA SMITH' and " - "'doe, john ma smith' reading the same way", + "rather than under the case signal", + shape=2), + Case("a_capitals_member_runs_over_the_name_word_behind_it", + "Doe, John MA Smith", + {"given": "John", "family": "Doe", "suffix": "MA Smith"}, + classification="fix(#785)", + ambiguities=("suffix-or-name",), + notes="S8: the capitals settle MA (S6), and a settled " + "member starts the credential's run as 'Doe, John PhD " + "Smith' does, so the name word behind it is the run's, " + "reported. 1.4.0 read middle 'Smith', suffix 'MA'; " + "2.0.0 through 2.6.0 middle 'MA Smith'", shape=2), Case("a_multi_token_piece_never_reaches_the_slot", "Doe, John MA y", @@ -6109,7 +6125,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "Jane", "middle": "nee Smith", "family": "Doe", "suffix": "MA"}, shape=2, classification="fix(#601)", - ambiguities=("suffix-or-name",), notes="#601: a marker in the given part after a family comma is an " "ordinary word (rules/M2). Before #601: given 'Jane', family " "'Doe', suffix 'MA', maiden 'Smith'. As pinned then: the row " @@ -6137,7 +6152,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "Jane", "family": "Doe", "suffix": "MA", "maiden": "Smith"}, classification="fix(#533)", - ambiguities=("suffix-or-name",), notes="the comma-less spelling of the row above, and the " "statement of the rule. The peel over the pieces as " "they stand takes 'MA', and the peel over the view the " @@ -6216,22 +6230,20 @@ def _check_cjk_shape_purity(self) -> None: {"given": "Jane", "family": "Doe", "suffix": "MA PhD", "maiden": "Smith"}, classification="fix(#533)", - ambiguities=("suffix-or-name",), - notes="the peel walks the run from the end, so 'PhD' is " - "settled vocabulary and 'MA' is the fork it reaches " - "behind it -- one report, not two. Rendered with a " - "SPACE: R1 derives suffix entries from the commas the " - "WRITER typed (#436/#437), where 1.4.0 wrote 'MA, " - "PhD'. 2.0.0 through 2.3.0 read maiden 'Smith MA' with " - "suffix 'PhD', which is the order-sensitivity the " - "issue reported", + notes="the peel walks the run from the end, so 'PhD' is settled " + "vocabulary and 'MA' is the member it reaches behind it, " + "settled by its capitals and silent since #785 (rules/S6). " + "Rendered with a SPACE: R1 derives suffix entries from the " + "commas the WRITER typed (#436/#437), where 1.4.0 wrote " + "'MA, PhD'. 2.0.0 through 2.3.0 read maiden 'Smith MA' with " + "suffix 'PhD', which is the order-sensitivity the issue " + "reported", shape=1), Case("the_credential_run_reads_the_same_written_the_other_way", "Jane Doe nee Smith PhD MA", {"given": "Jane", "family": "Doe", "suffix": "PhD MA", "maiden": "Smith"}, classification="fix(#289)", - ambiguities=("suffix-or-name",), notes="the control the row above needs, and the whole point " "of the issue: this spelling already stopped at the " "suffix WORD and read both credentials, so whether the " @@ -6244,24 +6256,26 @@ def _check_cjk_shape_purity(self) -> None: {"given": "Jane", "family": "Doe", "suffix": "JD", "maiden": "Smith Ma"}, classification="fix(#533)", - ambiguities=("suffix-or-name", "suffix-or-name"), - notes="both halves in one name: 'JD' is taken and reported " - "by assign where it peels it, 'Ma' is declined by its " - "writing and reported by the walk that kept it. TWO " - "reports, one per member, and never two for one word " - "-- the maiden pieces are gone before the chain runs. " - "1.4.0 read suffix 'Ma, JD'", + ambiguities=("suffix-or-name",), + notes="both halves in one name: 'JD' is taken by assign where it " + "peels it, settled by its capitals and silent since #785 " + "(rules/S6), and 'Ma' is declined by its writing and " + "reported by the walk that kept it. ONE report, and never " + "two for one word -- the maiden pieces are gone before the " + "chain runs. 1.4.0 read suffix 'Ma, JD'", shape=1), Case("two_members_ending_the_clause_report_once_each", - "Jane Doe nee Smith MA JD", - {"given": "Jane", "family": "Doe", "suffix": "MA JD", + "Jane Doe nee Smith ma jd", + {"given": "Jane", "family": "Doe", "suffix": "ma jd", "maiden": "Smith"}, classification="fix(#533)", ambiguities=("suffix-or-name", "suffix-or-name"), notes="the same pair with both members taken. The peel " - "resolves each in turn and assign reports both; the " + "resolves each in turn and both are reported; the " "walk reports none, the last maiden piece being an " - "ordinary name word", + "ordinary name word. Written in lower case since #785: " + "in capitals the members are settled and report nothing " + "(rules/S6)", shape=1), Case("the_dotted_member_ends_the_clause", "John Smith nee Jones R.A.I.", @@ -6323,21 +6337,19 @@ def _check_cjk_shape_purity(self) -> None: {"title": "Prof.", "given": "Jane", "family": "Doe", "suffix": "MA", "maiden": "Smith"}, classification="fix(#535)", - ambiguities=("suffix-or-name",), - notes="the H5 boundary #533 recorded, now closed: the walk " - "reads the end of the name through the trailing-title " - "chain as assign does (tail_reading), so 'Prof.' is a " - "stop and no longer hides the member in front of it, " - "which the credential stop then gives up as it does in " - "'Jane Doe nee Smith MA'. The report is assign's peel " - "of 'MA'. 2.3.0 read maiden 'Smith MA Prof.'", + notes="the H5 boundary #533 recorded, now closed: the walk reads " + "the end of the name through the trailing-title chain as " + "assign does (tail_reading), so 'Prof.' is a stop and no " + "longer hides the member in front of it, which the " + "credential stop then gives up as it does in 'Jane Doe nee " + "Smith MA'. Silent since #785, the capitals settling 'MA' " + "(rules/S6). 2.3.0 read maiden 'Smith MA Prof.'", shape=1), Case("a_title_in_front_of_the_member_is_the_other_spelling", "Jane Doe nee Smith Prof. MA", {"title": "Prof.", "given": "Jane", "family": "Doe", "suffix": "MA", "maiden": "Smith"}, classification="fix(#535)", - ambiguities=("suffix-or-name",), notes="the pair is the finding, and it now AGREES: with the " "title read as the chain reads it, 'Prof. MA' and " "'MA Prof.' give one answer, as they do at the given " @@ -6382,7 +6394,6 @@ def _check_cjk_shape_purity(self) -> None: {"title": "Prof.", "given": "Jane", "middle": "nee Smith", "family": "Doe", "suffix": "MA"}, shape=2, classification="fix(#601)", - ambiguities=("suffix-or-name",), notes="#601: a marker in the given part after a family comma is an " "ordinary word (rules/M2). Before #601: title 'Prof.', given " "'Jane', family 'Doe', suffix 'MA', maiden 'Smith'. As pinned " @@ -6566,7 +6577,6 @@ def _check_cjk_shape_purity(self) -> None: "Doe, Jane nee Smith Rev. MA Prof.", {"title": "Prof.", "given": "Jane", "middle": "nee Smith Rev.", "family": "Doe", "suffix": "MA"}, classification="fix(#601)", - ambiguities=("suffix-or-name",), notes="#601: a marker in the given part after a family comma is an " "ordinary word (rules/M2). Before #601: title 'Prof.', given " "'Jane', family 'Doe', suffix 'MA', maiden 'Smith Rev.'. As " @@ -6671,7 +6681,6 @@ def _check_cjk_shape_purity(self) -> None: "Jane Doe nee Smith i DO Prof.", {"title": "Prof.", "given": "Jane", "family": "Doe", "suffix": "i DO", "maiden": "Smith"}, classification="fix(#601)", - ambiguities=("suffix-or-name",), notes="#601: the clause ends at the clause-free name's trailing run, " "which the take consumes (rules/M2). Before #601: title " "'Prof.', given 'Jane', family 'Doe', maiden 'Smith i DO'. As " @@ -6712,7 +6721,7 @@ def _check_cjk_shape_purity(self) -> None: "Doe, Jane nee Smith MA St.", {"given": "Jane", "middle": "nee Smith", "family": "St. Doe", "suffix": "MA"}, classification="fix(#601)", - ambiguities=("particle-or-given", "suffix-or-name"), + ambiguities=("particle-or-given",), notes="#601: a marker in the given part after a family comma is an " "ordinary word (rules/M2). Before #601: given 'Jane', family " "'Doe', maiden 'Smith MA St.'. As pinned then: the same guard " @@ -6724,7 +6733,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "Jane", "family": "Doe", "suffix": "MA Ph. D.", "maiden": "Smith"}, classification="fix(#533)", - ambiguities=("suffix-or-name",), notes="the TRAILING reader's release check counts a piece " "group already flagged a credential ('Ph. D.', merged " "from two tokens) as read off, so 'MA Ph. D.' leaves " @@ -6733,26 +6741,42 @@ def _check_cjk_shape_purity(self) -> None: "Doe, Jane nee Smith MA Ph. D.", {"given": "Jane", "middle": "nee Smith", "family": "Doe", "suffix": "MA Ph. D."}, classification="fix(#601)", - ambiguities=("suffix-or-name",), notes="#601: a marker in the given part after a family comma is an " "ordinary word (rules/M2). Before #601: given 'Jane', family " "'Doe', suffix 'MA Ph. D.', maiden 'Smith'. As pinned then: the " "given-slot twin of the row above. 2.3.0 read maiden 'Smith " "MA', suffix 'Ph. D.'"), - Case("a_connective_behind_the_member_stops_the_peel", + Case("a_capitals_member_behind_the_maiden_name_takes_the_connective", "Jane Doe nee Smith MA y", - {"given": "Jane", "family": "Doe", "maiden": "Smith MA y"}, + {"given": "Jane", "family": "Doe", "maiden": "Smith", + "suffix": "MA y"}, + shape=1, + classification="fix(#785)", + ambiguities=("suffix-or-name",), + notes="S8: the capitals settle MA (S6) and start the " + "credential's run in the clause-free reading, which takes " + "the 'y' as 'Jane Doe nee Smith PhD y' does, so the " + "clause ends before MA (rules/M2) and the 'y' is " + "reported as the run's. 1.4.0 read middle 'Doe nee " + "Smith', last 'MA y'; 2.0.0 through 2.6.0 maiden 'Smith " + "MA y'"), + Case("a_connective_behind_the_member_stops_the_peel", + "Jane Doe nee Smith Ma y", + {"given": "Jane", "family": "Doe", "maiden": "Smith Ma y"}, classification="fix(#274)", ambiguities=(), notes="the recorded negative control for the PEEL's reach, " "measured 2026-09-19 rather than reasoned: the marker " - "pass runs before every join, so 'MA' and 'y' are two " + "pass runs before every join, so 'Ma' and 'y' are two " "pieces here and no joined one -- and 'y' is no " "suffix, so the peel takes nothing, the walk is never " "asked about the member behind it, and the clause " "keeps both words. The connective joins them a stage " "later, into the maiden name. Silent for the reason " - "the row below is: nothing was decided", + "the row below is: nothing was decided. Written in " + "Title case since #785: in capitals the member settles " + "and starts the credential's run, which takes the 'y' " + "(rules/S8), as 'Jane Doe nee Smith PhD y' reads", shape=1), Case("the_numeral_half_of_the_walk_is_unmoved", "Jane Doe nee Smith V", @@ -6842,7 +6866,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "Jane", "family": "Smith", "suffix": "MA", "maiden": "Jones and"}, classification="fix(#533)", - ambiguities=("suffix-or-name",), notes="an ACCEPTED COST. The take runs before the joins " "(#420), so the connective is a piece of its own when " "the walk stops and the maiden name ends on it; M2's " @@ -6854,7 +6877,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "Maria", "family": "Kowalska", "suffix": "MA", "maiden": "Nowak"}, classification="fix(#533)", - ambiguities=("suffix-or-name",), notes="the rule is about the marker's CLAUSE, not about a " "one-word marker: the Polish phrase entry (#434) " "reaches the same walk and the same stop. 1.4.0 read " @@ -6865,7 +6887,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "Jane", "family": "Doe", "suffix": "MA", "maiden": "Smith"}, classification="fix(#533)", - ambiguities=("suffix-or-name",), notes="a second vocabulary spelling, for the same reason as " "the row above: nothing here is about the word 'nee'", shape=1), @@ -6933,7 +6954,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "Jane", "middle": "nee Smith", "family": "Doe", "suffix": "DO"}, shape=2, classification="fix(#601)", - ambiguities=("suffix-or-name",), notes="#601: a marker in the given part after a family comma is an " "ordinary word (rules/M2). Before #601: given 'Jane', family " "'Doe', suffix 'DO', maiden 'Smith'. As pinned then: the same " @@ -6963,29 +6983,44 @@ def _check_cjk_shape_purity(self) -> None: notes="the recorded control for the row above. Unchanged, " "and it is what the clause form was measured against", shape=1), - Case("the_comma_floor_keeps_a_member_a_particle_follows", + Case("a_capitals_member_after_a_family_comma_clause_starts_the_run", "Doe, Jane nee Smith MA do", - {"given": "Jane", "middle": "nee Smith MA", "family": "do Doe"}, + {"given": "Jane", "middle": "nee Smith", "family": "Doe", + "suffix": "MA do"}, + shape=2, + classification="fix(#785)", + ambiguities=("suffix-or-name",), + notes="S8: the marker is an ordinary word after a family " + "comma (rules/M2), and the capitals settle MA (S6), " + "which starts the credential's run, taking the lone " + "member 'do' as the credential, reported; as 'Doe, Jane " + "nee Smith PhD do' reads. 1.4.0 read middle 'nee Smith', " + "suffix 'MA, do'; 2.0.0 through 2.3.0 maiden 'Smith MA " + "do', and 2.6.0 middle 'nee Smith MA', family 'do Doe'"), + Case("the_comma_floor_keeps_a_member_a_particle_follows", + "Doe, Jane nee Smith ma do", + {"given": "Jane", "middle": "nee Smith ma", "family": "do Doe"}, shape=2, classification="fix(#601)", ambiguities=("particle-or-given",), notes="#601: a marker in the given part after a family comma is an " "ordinary word (rules/M2). Before #601: given 'Jane', family " "'Doe', maiden 'Smith MA do'. As pinned then: where #531's " - "FLOOR earns its place, and a check that asked only about 'MA' " - "got this wrong: the take would leave 'Jane MA do', where 'do' " - "does not read as a suffix (P6 keeps it) so #531's slot reads " - "'MA' as a MIDDLE name -- releasing it from the clause would " - "move a word from one person's name into another's. With the " - "floor the clause keeps 'MA do' whole, and reports the 'do' it " - "kept. rules/S5's own 'Doe, John MA do' clause is the " - "reading this rests on"), + "FLOOR earns its place, and a check that asked only about the " + "member got this wrong: the take would leave 'Jane ma do', " + "where 'do' does not read as a suffix (P6 keeps it) so #531's " + "slot reads 'ma' as a MIDDLE name -- releasing it from the " + "clause would move a word from one person's name into " + "another's. With the floor the clause keeps 'ma do' whole, and " + "reports the 'do' it kept. Written in lower case since #785: " + "in capitals the member settles and starts the credential's " + "run (rules/S8), as 'Doe, John MA do' reads"), Case("the_clamp_never_takes_the_first_word_after_the_marker", "Doe, J. nee MA ba", {"given": "J.", "middle": "nee", "family": "Doe", "suffix": "MA ba"}, shape=2, classification="fix(#601)", - ambiguities=("suffix-or-name", "suffix-or-name"), + ambiguities=("suffix-or-name",), notes="#601: a marker in the given part after a family comma is an " "ordinary word (rules/M2). Before #601: given 'J.', family " "'Doe', suffix 'ba', maiden 'MA'. As pinned then: the floor is " @@ -7013,7 +7048,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "Jane", "middle": "Q. nee Smith", "family": "Doe", "suffix": "MA"}, shape=2, classification="fix(#601)", - ambiguities=("suffix-or-name",), notes="#601: a marker in the given part after a family comma is an " "ordinary word (rules/M2). Before #601: given 'Jane', middle " "'Q.', family 'Doe', suffix 'MA', maiden 'Smith'. As pinned " @@ -7025,7 +7059,6 @@ def _check_cjk_shape_purity(self) -> None: {"title": "Dr.", "given": "nee", "middle": "Smith", "family": "Doe", "suffix": "MA"}, shape=2, classification="fix(#601)", - ambiguities=("suffix-or-name",), notes="#601: a marker in the given part after a family comma is an " "ordinary word (rules/M2). Before #601: title 'Dr.', family " "'Doe', maiden 'Smith MA'. As pinned then: rules/M2's " @@ -7046,7 +7079,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "abdul", "middle": "nee Jones", "family": "Berg", "suffix": "MA"}, shape=2, classification="fix(#601)", - ambiguities=("suffix-or-name",), notes="#601: a marker in the given part after a family comma is an " "ordinary word (rules/M2). Before #601: given 'abdul', " "family 'Berg', maiden 'Jones MA'. As pinned then: the other " @@ -7073,7 +7105,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "Jane", "middle": "van der nee Smith", "family": "Berg", "suffix": "DO"}, shape=2, classification="fix(#601)", - ambiguities=("suffix-or-name",), notes="#601: a marker in the given part after a family comma is an " "ordinary word (rules/M2). Degenerate input, pinned to " "detect change rather than for a reading anyone wants " @@ -7092,7 +7123,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "Jane", "family": "Doe", "suffix": "DO DO", "maiden": "Smith"}, shape=2, classification="fix(#601)", - ambiguities=("suffix-or-name", "suffix-or-name"), notes="#601: the clause ends at the clause-free name's trailing run, " "which the take consumes (rules/M2). Before #601: given " "'Jane', family 'Doe', maiden 'Smith DO DO'. As pinned then: " @@ -7150,7 +7180,7 @@ def _check_cjk_shape_purity(self) -> None: {"given": "Jane", "family": "Doe", "suffix": "MA", "maiden": "Smith V"}, classification="fix(#533)", - ambiguities=("suffix-or-name", "suffix-or-name"), + ambiguities=("suffix-or-name",), notes="both stops read the TRAILING word, so the numeral is " "not the word either fork asks about: the acronym fork " "stops at 'MA' and 'V' stays maiden text behind it. " @@ -7190,31 +7220,31 @@ def _check_cjk_shape_purity(self) -> None: {"given": "Jane", "family": "Doe", "suffix": "MA", "maiden": "Smith"}, classification="parity", - ambiguities=("suffix-or-name",), - notes="the boundary cuts both ways: the span is the maiden " - "name whole, and a member the writer left OUTSIDE it " - "is an ordinary trailing credential that assign peels " - "and reports. The control for the two rows above", + notes="the boundary cuts both ways: the span is the maiden name " + "whole, and a member the writer left OUTSIDE it is an " + "ordinary trailing credential that assign peels, settled by " + "its capitals and silent since #785 (rules/S6). The control " + "for the two rows above", shape=2), Case("two_released_members_each_get_their_own_report", - "Jane Doe nee Smith Ma JD", - {"given": "Jane", "family": "Doe", "suffix": "JD", + "Jane Doe nee Smith Ma jd", + {"given": "Jane", "family": "Doe", "suffix": "jd", "maiden": "Smith Ma"}, classification="fix(#533)", ambiguities=("suffix-or-name", "suffix-or-name"), notes="the shape that puts BOTH suffix-or-name emitters on " "one parse: the clause keeps 'Ma' on its lean and " - "reports it, assign peels 'JD' and reports that, and " + "reports it, the peel takes 'jd' and reports that, and " "the two name different tokens -- which is what " "test_properties' span test needs to actually " - "exercise its own check", + "exercise its own check. Written 'jd' since #785: a " + "capitals 'JD' is settled and reports nothing (rules/S6)", shape=2), Case("no_trailing_rule_reads_the_family_segments_clause", "Smith nee Jones, Jane MA", {"given": "Jane", "family": "Smith", "suffix": "MA", "maiden": "Jones"}, classification="fix(#531)", - ambiguities=("suffix-or-name",), notes="the clause is in segment 0 of a family comma, where " "the comma has ALREADY named the family: those words " "are family text and a stop would hand one to `family` " @@ -7242,7 +7272,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "Jane", "middle": "nee Smith", "family": "Doe", "suffix": "MA"}, policy=Policy(lenient_comma_suffixes=False), classification="fix(#601)", - ambiguities=("suffix-or-name",), notes="#601: a marker in the given part after a family comma is an " "ordinary word (rules/M2). Before #601: given 'Jane', family " "'Doe', suffix 'MA', maiden 'Smith'. As pinned then: the knob " @@ -7253,7 +7282,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "Jane", "middle": "nee Smith", "family": "Doe", "suffix": "MA"}, policy=Policy(name_order=FAMILY_FIRST), classification="fix(#601)", - ambiguities=("suffix-or-name",), notes="#601: a marker in the given part after a family comma is an " "ordinary word (rules/M2). Before #601: given 'Jane', family " "'Doe', suffix 'MA', maiden 'Smith'. As pinned then: name_order " @@ -7267,7 +7295,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "Jane", "middle": "nee Smith", "family": "Doe", "suffix": "MA"}, policy=Policy(name_order=FAMILY_FIRST_GIVEN_LAST), classification="fix(#601)", - ambiguities=("suffix-or-name",), notes="#601: a marker in the given part after a family comma is an " "ordinary word (rules/M2). Before #601: given 'Jane', family " "'Doe', suffix 'MA', maiden 'Smith'. As pinned then: the third " @@ -7319,11 +7346,10 @@ def _check_cjk_shape_purity(self) -> None: "the new branch never sees it and the middle initial " "survives. The control that pins the restriction's " "scope after #531 narrowed nothing about it"), - Case("the_no_name_gate_path_still_reports_exactly_once", + Case("the_no_name_gate_path_reads_the_capitals_at_the_comma", "Doe, MA", {"family": "Doe", "suffix": "MA"}, classification="fix(#289)", - ambiguities=("suffix-or-name",), notes="the reading is the_lean_reaches_the_post_comma_slot's " "('Smith, MA') word for word, so it carries that row's " "classification: 1.4.0 AND 2.0.0 both read first 'MA', " @@ -7332,22 +7358,22 @@ def _check_cjk_shape_purity(self) -> None: "UNCHANGED by #531, and structurally unreachable by the new " "emitter rather than luckily missed: segment 1 holds " "no name word, so `_comma.decide` binds every role at " - "group's head (#613) and reports the member there, and " - "assign's placement loop is handed no piece of it. So " - "the slot the first-piece report owns and the slot the " - "new one owns cannot both fire -- which is why no token is " - "ever reported twice", + "group's head (#613), and assign's placement loop is " + "handed no piece of it. So the slot the first-piece " + "report owns and the slot the new one owns cannot both " + "fire. It reported the member there once until #785, " + "whose capitals settle it in silence (rules/S6)", shape=2), - Case("the_no_name_gate_path_with_a_run_still_reports_once", + Case("the_no_name_gate_path_with_a_run_reads_the_capitals", "Doe, MA PhD", {"family": "Doe", "suffix": "MA PhD"}, classification="fix(#289)", - ambiguities=("suffix-or-name",), notes="the same gate path with a credential run behind the " "member, and the same classification as its bare twin " "above: 1.4.0 and 2.0.0 alike read first 'MA', suffix " - "'PhD'. One report, for the one class member, exactly " - "as before #531", + "'PhD'. One report, for the one class member, from #531 " + "until #785, whose capitals settle it in silence " + "(rules/S6)", shape=2), Case("a_title_led_segment_consumes_the_member_in_silence", "Doe, Dr. MA", @@ -7374,14 +7400,14 @@ def _check_cjk_shape_purity(self) -> None: "Doe, John DO", {"given": "John", "family": "Doe", "suffix": "DO"}, classification="fix(#531)", - ambiguities=("suffix-or-name",), notes="'do' is the one class member that is also particle " - "vocabulary, so this slot and P6's attachment want the " - "same word. A POSITIVE credential lean -- an all-caps " - "member in a name written in more than one case -- is " - "what takes it, and P6 stands down. 1.4.0 read suffix " - "'DO'. Reports ONCE: where this reading wins, P6 has " - "nothing to report", + "vocabulary, so this slot and P6's attachment want the same " + "word. A POSITIVE credential lean -- an all-caps member in " + "a name written in more than one case -- is what takes it, " + "and P6 stands down. 1.4.0 read suffix 'DO'. Reports " + "nothing since #785: the capitals settle the clash with the " + "name reading (rules/S6), leaving the particle reading for " + "P6, which stands down where this reading wins", shape=2), Case("the_particle_rule_keeps_the_lower_case_member", "Nascimento, Edson Arantes do", @@ -7467,18 +7493,34 @@ def _check_cjk_shape_purity(self) -> None: "this", shape=2), Case("the_particle_carve_out_silences_the_member_in_front", - "Doe, John MA do", - {"given": "John", "middle": "MA", "family": "do Doe"}, + "Doe, John ma do", + {"given": "John", "middle": "ma", "family": "do Doe"}, classification="fix(#380)", ambiguities=("particle-or-given",), notes="the carve-out cascades: lower-case 'do' leans " "nothing, so this walk does not read it as a suffix -- " "and a piece the walk refuses ENDS the run, so the " - "all-caps 'MA' in front of it is never asked and never " - "reports, capitals and all. P6 then attaches 'do' a " - "stage later, too late to re-open the question. One " - "report, P6's. UNCHANGED by #531: byte-identical at " - "cc78c960 and on the 2.3.0 wheel (2026-09-19)", + "lower-case 'ma' in front of it is never asked and never " + "reports. P6 then attaches 'do' a stage later, too late " + "to re-open the question. One report, P6's. Until #785 " + "the row was written 'Doe, John MA do', and the capitals " + "were never asked either; they now start the " + "credential's run (the row below). 1.4.0 read suffix " + "'ma, do'", + shape=2), + Case("a_capitals_member_takes_the_particle_behind_it", + "Doe, John MA do", + {"given": "John", "family": "Doe", "suffix": "MA do"}, + classification="fix(#785)", + ambiguities=("suffix-or-name",), + notes="S8: the capitals settle MA (S6) and start the " + "credential's run, which reads the lone member 'do' " + "behind it as the credential, as 'Doe, John PhD do' " + "does (S8's Except: a lone member of the class is not " + "the particle P6 attaches), reported as that member's " + "fork. 1.4.0 read suffix 'MA, do', 2.0.0 middle " + "'MA do', and 2.3.0 through 2.6.0 middle 'MA', family " + "'do Doe'", shape=2), Case("one_case_keeps_the_particle_on_the_real_record", "NASCIMENTO, EDSON ARANTES DO", @@ -7520,32 +7562,31 @@ def _check_cjk_shape_purity(self) -> None: "Doe, John van MA", {"given": "John", "family": "van Doe", "suffix": "MA"}, classification="fix(#531)", - ambiguities=("particle-or-given", "suffix-or-name"), - notes="A FAMILY MOVE, found by measurement and accepted by " - "Derek 2026-09-18: today this is middle 'van MA', " - "silent. Once 'MA' becomes a suffix, P6's own walk " - "looks past that post-nominal, finds 'van' as a " - "trailing all-particle run and attaches it. That is " - "exactly 'Berg, Jan van Jr.'s reading, arriving " - "through a shape it could not reach before. TWO " - "reports, one per fork, and neither is this slot " - "reporting twice"), + ambiguities=("particle-or-given",), + notes="A FAMILY MOVE, found by measurement and accepted by Derek " + "2026-09-18: today this is middle 'van MA', silent. Once " + "'MA' becomes a suffix, P6's own walk looks past that " + "post-nominal, finds 'van' as a trailing all-particle run " + "and attaches it. That is exactly 'Berg, Jan van Jr.'s " + "reading, arriving through a shape it could not reach " + "before. ONE report since #785, P6's, the capitals settling " + "'MA' (rules/S6)"), Case("the_slot_reaches_past_an_unambiguous_suffix_particle", "Doe, John MA vd", {"given": "John", "family": "vd Doe", "suffix": "MA"}, classification="fix(#531)", - ambiguities=("suffix-or-name", "suffix-or-name"), - notes="the row above with the particle BEHIND the member " - "instead of in front, and the pair says the cascade is " - "not about where the particle stands: 'vd' is claimed " - "outright by the suffix vocabulary, so this walk reads " - "past it and takes 'MA', and P6 then attaches 'vd' " - "over that reading -- reporting in the kind naming the " - "reading it OVERRODE, which is why both reports here " - "are `suffix-or-name` where 'Doe, John van MA' gives " - "one of each. Today middle 'MA', family 'vd Doe', one " - "report ('Berg, Jan vd' is that reading with nothing " - "in front of the particle)"), + ambiguities=("suffix-or-name",), + notes="the row above with the particle BEHIND the member instead " + "of in front, and the pair says the cascade is not about " + "where the particle stands: 'vd' is claimed outright by the " + "suffix vocabulary, so this walk reads past it and takes " + "'MA', and P6 then attaches 'vd' over that reading -- " + "reporting in the kind naming the reading it OVERRODE, " + "which is why its report is `suffix-or-name` where 'Doe, " + "John van MA' gives `particle-or-given`; since #785 it is " + "the only one, the capitals settling 'MA' (rules/S6). Today " + "middle 'MA', family 'vd Doe', one report ('Berg, Jan vd' " + "is that reading with nothing in front of the particle)"), # ---- policy rows. Every one is _CORE_ONLY (see # tests/v2/test_facade_cases.py) -------------------------------- Case("the_dotted_slot_reports_with_the_switch_off", @@ -7577,7 +7618,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "John", "family": "Doe", "suffix": "MA"}, policy=Policy(lenient_comma_suffixes=False), classification="fix(#531)", - ambiguities=("suffix-or-name",), notes="`lenient_comma_suffixes` governs #144's initial-veto " "predicate, which this branch does not go through -- " "so the strict knob reads this slot exactly as the " @@ -7588,7 +7628,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "John", "family": "Doe", "suffix": "MA"}, policy=Policy(name_order=FAMILY_FIRST), classification="fix(#531)", - ambiguities=("suffix-or-name",), notes="a family comma names the family, so the declared " "order arbitrates nothing here. Measured identical " "under all three orders"), @@ -7597,7 +7636,6 @@ def _check_cjk_shape_purity(self) -> None: {"given": "John", "family": "Doe", "suffix": "MA"}, policy=Policy(name_order=FAMILY_FIRST_GIVEN_LAST), classification="fix(#531)", - ambiguities=("suffix-or-name",), notes="the third order, for the same reason as the second"), # ---- caseless scripts. tolerated=True, NOT a shape tag: # cases.py's __post_init__ hard-errors on a composed CJK comma @@ -7898,13 +7936,11 @@ def _check_cjk_shape_purity(self) -> None: Case("particles_side_by_side_in_a_credential_part_stay_credentials", "De La Cruz, DO DO DO", {"family": "De La Cruz", "suffix": "DO DO DO"}, - ambiguities=("suffix-or-name",), classification="fix(#613)", notes="decision (2) of #613: the part is bound before group's chain can join the particles; b547c247 read given 'DO', middle 'DO DO'"), Case("the_chain_cannot_take_a_title_from_a_credential_part", "De La Cruz, MA DO Mr.", {"title": "Mr.", "family": "De La Cruz", "suffix": "MA DO"}, - ambiguities=("suffix-or-name",), classification="fix(#613)", notes="decision (3) of #613: b547c247's chain joined 'DO Mr.' and read given 'MA', middle 'DO Mr.'"), Case("a_suffixed_family_before_a_comma_takes_a_lone_given_initial", @@ -7953,7 +7989,6 @@ def _check_cjk_shape_purity(self) -> None: Case("a_cjk_honorific_behind_a_one_word_comma_peels_before_credentials", "김민준씨, MA PhD", {"family": "김민준", "suffix": "씨, MA PhD"}, - ambiguities=("suffix-or-name",), classification="fix(#613)", tolerated=True, notes="rules/W3 tolerated: peels as '김민준씨, PhD' does; b547c247 kept family '김민준씨'"), @@ -7992,7 +8027,6 @@ def _check_cjk_shape_purity(self) -> None: Case("a_title_and_one_cjk_name_word_before_a_credential", "Dr 김민준, MA", {"title": "Dr", "given": "민준", "family": "김", "suffix": "MA"}, - ambiguities=("suffix-or-name",), classification="fix(#613)", tolerated=True, notes="rules/W3 tolerated, decided with Derek (#613's " @@ -8004,14 +8038,15 @@ def _check_cjk_shape_purity(self) -> None: Case("an_empty_family_part_declines_no_empty_particle_run", ", Jane PhD do MA", {"given": "Jane", "suffix": "PhD do MA"}, - ambiguities=("suffix-or-name", "suffix-or-name"), + ambiguities=("suffix-or-name",), classification="fix(#613)", notes="post_rules' P6 site behind an empty family part can find " "an EMPTY particle run (the walk stops short of 'do', read " "as the credential), and the third exception must decline " "nothing there; without its empty-run guard it reported a " - "third fork on 'MA' (#613's PR simplification pass). Two " - "reports, as b547c247 gave"), + "third fork on 'MA' (#613's PR simplification pass). One " + "report since #785, the capitals settling 'MA' (rules/S6); " + "b547c247 gave two"), Case("a_glued_honorific_in_a_postnominal_part_stays_glued", "John Smith, Dr. J.씨", {"title": "Dr.", "given": "John", "family": "Smith", @@ -8504,11 +8539,11 @@ def _check_cjk_shape_purity(self) -> None: Case("semicolon_glued_ambiguous_credential", "John Smith, MA; PhD", {"given": "John", "family": "Smith", "suffix": "MA; PhD"}, classification="fix(semicolon-suffix-delimiter)", - ambiguities=("suffix-or-name",), notes="a word of the ambiguous class with a semicolon glued " - "carries the class's tags, so C4's count reads it as " - "'John Smith, MA PhD' does, and reports it the same way " - "(rules/C10). 1.4.0 through 2.5.0 read given 'MA;'"), + "carries the class's tags, so C4's count reads it as 'John " + "Smith, MA PhD' does, and reads it the same way " + "(rules/C10), settled by its capitals and silent since #785 " + "(rules/S6). 1.4.0 through 2.5.0 read given 'MA;'"), Case("semicolon_glued_to_a_split_credential_keeps_the_listing_form", "John Smith, Ph. D.; MD", {"title": "Ph.", "given": "D.;", "family": "John Smith", @@ -11200,8 +11235,7 @@ def _check_cjk_shape_purity(self) -> None: "keeps the suffix"), Case("ma_after_a_full_name_and_a_comma_is_the_credential", "John Smith, MA", - {"given": "John", "family": "Smith", "suffix": "MA"}, - ambiguities=("suffix-or-name",)), + {"given": "John", "family": "Smith", "suffix": "MA"}), Case("sa_after_a_comma_with_no_head_is_the_given_name", ", SA", {"given": "SA"}, classification="fix(#857)", @@ -11223,7 +11257,6 @@ def _check_cjk_shape_purity(self) -> None: Case("audit_bare_caps_do_after_comma_is_a_credential", "Smith, DO", {"family": "Smith", "suffix": "DO"}, classification="fix(#289)", - ambiguities=("suffix-or-name",), notes="'do' left TITLES but was already AMBIGUOUS; the period " "gate handles the real collision, which is that 'Do' is " "a name -- but the bare, all-caps spelling is now read " @@ -11693,14 +11726,15 @@ def _check_cjk_shape_purity(self) -> None: Case("title_word_trailing_is_transparent_to_the_suffix_peel", "John Smith MA Prof.", {"title": "Prof.", "given": "John", "family": "Smith", - "suffix": "MA"}, ambiguities=("suffix-or-name",), + "suffix": "MA"}, classification="fix(#316)", notes="the principle the two peels serve: 'X Prof. Y' reads " - "exactly as 'X Y' reads, plus the title. This is " - "'John Smith MA' -- suffix 'MA', ONE report -- because " - "the reports come from the single peel over the " - "spliced pieces. Collecting both peels' picks instead " - "reported the same coin flip twice"), + "exactly as 'X Y' reads, plus the title. This is 'John " + "Smith MA' -- suffix 'MA', which reported once before #785 " + "settled its capitals (rules/S6), because the reports come " + "from the single peel over the spliced pieces. Collecting " + "both peels' picks instead reported the same coin flip " + "twice"), # MOVED by #289, not deleted: spliced, this is 'John MA' plus a # title, and 'MA' is written in capitals inside a mixed-case name # -- the caps lean now takes it with no words to spare where the @@ -11708,7 +11742,7 @@ def _check_cjk_shape_purity(self) -> None: Case("title_word_trailing_run_is_read_to_a_fixed_point", "John Prof. MA Prof.", {"title": "Prof. Prof.", "family": "John", "suffix": "MA"}, - ambiguities=("suffix-or-name",), classification="fix(#316)", + classification="fix(#316)", notes="transparency for a SECOND title, which peel and chain " "reading to a FIXED POINT buys and one re-peel did " "not: the chain takes the last 'Prof.', the peel over " @@ -11727,7 +11761,7 @@ def _check_cjk_shape_purity(self) -> None: Case("title_word_trailing_reads_as_the_leading_title_does", "John Prof. MA", {"title": "Prof.", "family": "John", "suffix": "MA"}, - ambiguities=("suffix-or-name",), classification="fix(#316)", + classification="fix(#316)", notes="the same principle where the peel's answer DEPENDS on " "the spliced list: this is 'John MA' plus a title and " "reads exactly as 'Prof. John MA' does. #289 moved this " @@ -11926,7 +11960,7 @@ def _check_cjk_shape_purity(self) -> None: Case("title_word_trailing_ahead_of_a_reserved_acronym", "John Smith Prof. MA", {"title": "Prof.", "given": "John", "family": "Smith", - "suffix": "MA"}, ambiguities=("suffix-or-name",), + "suffix": "MA"}, classification="fix(#316)", notes="'John Smith MA' plus a title, from the other " "direction: S2's peel takes the acronym BEFORE the " @@ -11968,7 +12002,7 @@ def _check_cjk_shape_purity(self) -> None: Case("title_word_trailing_behind_a_bound_pair_at_the_peel_reserve", "abdul rahman MA Prof.", {"title": "Prof.", "given": "abdul", "family": "rahman", - "suffix": "MA"}, ambiguities=("suffix-or-name",), + "suffix": "MA"}, classification="fix(#316)", notes="the row above's shape at S4's bare-ambiguous reserve, " "which is the one place the reserve's own model of " @@ -12182,7 +12216,7 @@ def _check_cjk_shape_purity(self) -> None: Case("a_second_title_particle_leads_the_chain", "Freiherr St van Berg MA", {"title": "Freiherr St", "family": "van Berg", "suffix": "MA"}, - ambiguities=("particle-or-given", "suffix-or-name"), + ambiguities=("particle-or-given",), classification="fix(#624)", notes="of two title-particles at the head the second leads the " "chain (rules/P4), so both are titles and 'van Berg' " @@ -12447,8 +12481,7 @@ def _check_cjk_shape_purity(self) -> None: Case("a_weighed_word_behind_a_particle_counts_as_a_word", "Freiherr von Berg MA X.Y.Z.", {"title": "Freiherr", "family": "von Berg", "suffix": "MA X.Y.Z."}, - ambiguities=("particle-or-given", "suffix-or-name", - "suffix-or-name"), + ambiguities=("particle-or-given", "suffix-or-name"), classification="fix(#516)", notes="'MA' is a word the reading weighs, so it counts as a word " "in front of 'X.Y.Z.' though the chain would join it (#620's " @@ -12492,7 +12525,6 @@ def _check_cjk_shape_purity(self) -> None: "Freiherr St John Smith MA", {"title": "Freiherr St", "given": "John", "family": "Smith", "suffix": "MA"}, - ambiguities=("suffix-or-name",), classification="fix(#624)", notes="#624's example: as 'Dr. St John Smith' and 'Sir St John " "Smith' read, the second title-particle is a title and " diff --git a/tests/v2/pipeline/test_assign.py b/tests/v2/pipeline/test_assign.py index 0e9a4b276..b8fdc67ba 100644 --- a/tests/v2/pipeline/test_assign.py +++ b/tests/v2/pipeline/test_assign.py @@ -120,12 +120,19 @@ def test_the_comma_path_reports_its_ambiguous_reading_once() -> None: # fork IS consulted at this comma and reports exactly once -- # which is also true with the switch OFF, the shape tag going on # either way (its own case rows pin both). - for text in ("Smith, MA", "Smith, Ma", "Smith, ma", "John Smith, MA", - "John Smith, Ed", "毛泽东, MA", "Smith, MA PhD", + # Capitals in a name of mixed case settle the word, so those + # spellings report nothing (rules/S6, #785); the name written in + # one case reaches the same fork with no writing to settle it. + for text in ("SMITH, MA", "Smith, Ma", "Smith, ma", "JOHN SMITH, MA", + "John Smith, Ed", "毛泽东, MA", "SMITH, MA PHD", "Smith, A.B."): kinds = [a.kind.value for a in _assigned( text, lexicon=Lexicon.default()).ambiguities] assert kinds.count("suffix-or-name") == 1, (text, kinds) + for text in ("Smith, MA", "John Smith, MA", "Smith, MA PhD"): + kinds = [a.kind.value for a in _assigned( + text, lexicon=Lexicon.default()).ambiguities] + assert "suffix-or-name" not in kinds, (text, kinds) # a name outside the class reports nothing at its comma, as every # release before this one for text in ("Smith, John", "John Smith, PhD", "Smith, Dr."): @@ -139,12 +146,15 @@ def test_the_family_comma_report_detail_is_verbatim() -> None: # `_comma.decide` emits where it binds the part at group's head # (#613; assign's family-comma emitter keeps the given-name half, # and the structure flip's report is pinned in test_comma.py). - # Pinned verbatim for the same reason. - out = _assigned("Smith, MA", lexicon=Lexicon.default()) + # Pinned verbatim for the same reason. Capitals settle 'Smith, MA' + # (rules/S6, #785), so the half is reached by a generation word its + # lower case leans to the suffix, opening a part the whole name in + # front reads as a credential run of words it settled. + out = _assigned("Alexandre Dumas, fils MA", lexicon=Lexicon.default()) (amb,) = [a for a in out.ambiguities if a.kind is AmbiguityKind.SUFFIX_OR_NAME] assert amb.detail == ( - "'MA' after the comma is also an ordinary name word; read as " + "'fils' after the comma is also an ordinary name word; read as " "a credential") @@ -172,7 +182,10 @@ def test_jack_ma_s_two_detail_strings_are_verbatim() -> None: # which way 'Jack MA' flips, not that it reports. Pinning both # strings verbatim: the credential lean also turns 'Jack' into the # only name word left, which is a SECOND fork (GIVEN_OR_FAMILY) - # this one row now calls. + # this one row now calls. Since #785 the capitals settle 'MA', so + # only that second fork reports (rules/S6); the trailing slot's + # string is pinned on the name written in one case, which the + # count decides. details = {a.kind.value: a.detail for a in _reported("Jack MA", lexicon=Lexicon.default())} assert details == { @@ -180,6 +193,11 @@ def test_jack_ma_s_two_detail_strings_are_verbatim() -> None: "'Jack' is the only name word and nothing else decides " "it; read as a given name by convention, which follows " "the read order"), + } + details = {a.kind.value: a.detail + for a in _reported("JOHN SMITH MA", + lexicon=Lexicon.default())} + assert details == { "suffix-or-name": ( "'MA' written without periods is both a post-nominal and " "an ordinary name; read as a suffix rather than a name " @@ -876,16 +894,24 @@ def test_the_shared_given_slot_predicate_reads_the_lean_three_ways( def test_a_name_word_behind_the_member_ends_the_run() -> None: - out = _assigned("Doe, John MA Smith", lexicon=Lexicon.default()) - assert _by_role(out, Role.MIDDLE) == "MA Smith" + # in one case: in a name of mixed case the capitals settle 'MA', + # which then starts the credential's run instead (rules/S8, #785) + out = _assigned("DOE, JOHN MA SMITH", lexicon=Lexicon.default()) + assert _by_role(out, Role.MIDDLE) == "MA SMITH" assert not [a for a in out.ambiguities if a.kind is AmbiguityKind.SUFFIX_OR_NAME] + out = _assigned("Doe, John MA Smith", lexicon=Lexicon.default()) + assert _by_role(out, Role.SUFFIX) == "MA Smith" def test_two_members_in_the_trailing_run_report_once_each() -> None: - out = _assigned("Doe, John MA JD", lexicon=Lexicon.default()) + out = _assigned("DOE, JOHN MA JD", lexicon=Lexicon.default()) assert len([a for a in out.ambiguities if a.kind is AmbiguityKind.SUFFIX_OR_NAME]) == 2 + # and none at all where the capitals settled both (rules/S6) + out = _assigned("Doe, John MA JD", lexicon=Lexicon.default()) + assert not [a for a in out.ambiguities + if a.kind is AmbiguityKind.SUFFIX_OR_NAME] def test_the_no_name_gate_path_still_reports_exactly_once() -> None: @@ -894,23 +920,31 @@ def test_the_no_name_gate_path_still_reports_exactly_once() -> None: word, so `_comma.decide` binds it at group's head and reports the member there (#613), and assign's placement loop is handed no piece of it -- so the new emitter is unreachable on exactly the - path the first-piece report owns.""" - for text in ("Doe, MA", "Doe, MA PhD", "Doe, MA JD"): + path the first-piece report owns. A member its capitals settle + reports nothing on that path (rules/S6, #785), so the path is + reached by one a credential in front speaks for (rules/S7).""" + for text in ("Doe, PhD Ma", "Doe, PhD Ma JD", "Doe, PhD ma"): out = _assigned(text, lexicon=Lexicon.default()) assert len([a for a in out.ambiguities if a.kind is AmbiguityKind.SUFFIX_OR_NAME]) == 1, text + for text in ("Doe, MA", "Doe, MA PhD", "Doe, MA JD"): + out = _assigned(text, lexicon=Lexicon.default()) + assert not [a for a in out.ambiguities + if a.kind is AmbiguityKind.SUFFIX_OR_NAME], text def test_the_trailing_given_slot_detail_is_verbatim() -> None: """Both branches, verbatim -- only the string distinguishes them, - the kind being the same either way.""" + the kind being the same either way. The credential branch is read + in one case: in a name of mixed case its capitals settle 'MA' and + it reports nothing (rules/S6, #785).""" detail = { text: [a.detail for a in _assigned(text, lexicon=Lexicon.default()).ambiguities if a.kind is AmbiguityKind.SUFFIX_OR_NAME] - for text in ("Doe, John MA", "Doe, John Ma") + for text in ("DOE, JOHN MA", "Doe, John Ma") } - assert detail["Doe, John MA"] == [ + assert detail["DOE, JOHN MA"] == [ "'MA' ending the given part is also an ordinary name word; " "read as a credential"] assert detail["Doe, John Ma"] == [ diff --git a/tests/v2/pipeline/test_comma.py b/tests/v2/pipeline/test_comma.py index 48a316f86..ce88dee5c 100644 --- a/tests/v2/pipeline/test_comma.py +++ b/tests/v2/pipeline/test_comma.py @@ -164,8 +164,9 @@ def test_the_structure_flip_reports_a_verbatim_detail() -> None: # family-comma path since 2.3 ("Berg, Jan vd"). Pinned verbatim so # a wording edit is a deliberate one, # not a silent drift the case table's looser `ambiguities=` tuple - # check would never catch. - state = _decided("John Smith, MA") + # check would never catch. In one case: capitals in a name of mixed + # case settle the word and the flip reports nothing (rules/S6, #785). + state = _decided("JOHN SMITH, MA") (amb,) = [a for a in state.ambiguities if a.kind is AmbiguityKind.SUFFIX_OR_NAME] assert amb.detail == ( @@ -177,7 +178,7 @@ def test_the_structure_flip_reports_a_verbatim_detail() -> None: def test_the_structure_flip_report_counts_the_real_pre_comma_words() -> None: # The count is INTERPOLATED, not a hardcoded "two": a three-word # pre-comma part reports its own count. - state = _decided("John Q. Public, MA") + state = _decided("JOHN Q. PUBLIC, MA") (amb,) = [a for a in state.ambiguities if a.kind is AmbiguityKind.SUFFIX_OR_NAME] assert "holds 3 name words" in amb.detail diff --git a/tests/v2/pipeline/test_group.py b/tests/v2/pipeline/test_group.py index 195a8762a..ab172ca92 100644 --- a/tests/v2/pipeline/test_group.py +++ b/tests/v2/pipeline/test_group.py @@ -1848,7 +1848,9 @@ def test_a_trailing_title_does_not_hide_the_suffix_run_from_the_join( "title": "Prof.", "given": "John", "middle": "Quincy", "family": "Adams", "suffix": "i MA", "nickname": "", "maiden": ""} assert name.initials() == "J. Q. A." - assert [a.kind.value for a in name.ambiguities] == ["suffix-or-name"] + # 'MA' in capitals in a name of mixed case is settled by its + # writing, so nothing reports it (rules/S6, #785) + assert name.ambiguities == () # a LOWER-CASE title is the same shape and reaches the same # bound: the vocabulary lookup is folded, so 'prof.' peels like # 'Prof.' diff --git a/tests/v2/pipeline/test_pieces.py b/tests/v2/pipeline/test_pieces.py index 554561119..8bf9fc80c 100644 --- a/tests/v2/pipeline/test_pieces.py +++ b/tests/v2/pipeline/test_pieces.py @@ -349,14 +349,17 @@ def test_peel_trailing_takes_the_three_lean_outcomes() -> None: # three: a CREDENTIAL lean consumes with no words to spare, a NAME # lean declines WITH words to spare, and no lean at all leaves # rules/S4's count deciding exactly as it did. - # Every case still REPORTS: the pick is appended either way, which - # is what the fork's report is built from. + # A credential lean on a listed acronym is the writing settling the + # clash (rules/S6, #785): taken as any suffix word is, and no pick, + # so nothing reports it. Every other case still REPORTS: the pick + # is appended either way, which is what the fork's report is built + # from. for text, one_case, names, picked in ( - ("Jack MA", False, 1, True), # credential lean, k == 2 + ("Jack MA", False, 1, False), # credential lean, k == 2 ("Jack Ma", False, 2, True), # name lean, k == 2 ("Jack MA", True, 2, True), # one case: today's count ("John Smith Ma", False, 3, True), # name lean, k == 3 - ("John Smith MA", False, 2, True), # credential lean + ("John Smith MA", False, 2, False), # credential lean ("JOHN SMITH MA", True, 2, True), # one case: the count ): rest, pieces, ptags, tokens = _peel_inputs(text) @@ -881,10 +884,24 @@ def test_a_title_word_never_starts_the_run() -> None: def test_the_ambiguous_class_and_initials_do_not_start_the_run() -> None: - assert parse("John Smith MA Jones").family == "Jones" + assert parse("John Smith Ma Jones").family == "Jones" + assert parse("JOHN SMITH MA JONES").family == "JONES" assert parse("John Smith V Jones").family == "Jones" +def test_a_member_its_capitals_settle_starts_the_run() -> None: + # rules/S8 (#785): written in capitals in a name of mixed case, a + # listed member marks the post-nominals as its dotted spelling does; + # its particle reading still refuses 'DO', the clash settled being + # the one with the name reading alone + for written in ("MA", "M.A."): + name = parse(f"John Smith {written} Jones") + assert (name.family, name.suffix) == ("Smith", f"{written} Jones") + assert [a.kind.value for a in name.ambiguities] == [ + "suffix-or-name"] + assert parse("John Smith DO Jones").family == "DO Jones" + + def test_a_joined_connective_does_not_start_the_run() -> None: assert parse("Josep Carod i Rovira").suffix == "" diff --git a/tests/v2/pipeline/test_post_rules.py b/tests/v2/pipeline/test_post_rules.py index 6c0792ea5..2ce0a9f3c 100644 --- a/tests/v2/pipeline/test_post_rules.py +++ b/tests/v2/pipeline/test_post_rules.py @@ -1159,11 +1159,15 @@ def test_p6_declines_a_credential_lean_class_member() -> None: """P6 keys on VOCABULARY, not role, so assign's suffix role would otherwise be silently overridden -- verified by running the change's assign half alone, which left 'Doe, John DO' family - 'DO Doe'.""" + 'DO Doe'. Its capitals settle the credential (rules/S6, #785), which + silences the report and must not touch the particle reading P6 + asks about: a retag did, giving family 'DO Doe' again.""" name = parse("Doe, John DO") assert name.suffix == "DO" assert name.family == "Doe" - assert [a.kind.value for a in name.ambiguities] == ["suffix-or-name"] + assert name.ambiguities == () + # the Title-case twin, which no writing settles: P6 attaches it + assert parse("Doe, John Do").family == "Do Doe" def test_p6_still_claims_an_unambiguous_suffix_particle() -> None: diff --git a/tests/v2/rules_budget.json b/tests/v2/rules_budget.json index 94d447af9..315d8206e 100644 --- a/tests/v2/rules_budget.json +++ b/tests/v2/rules_budget.json @@ -11,7 +11,7 @@ "C8": {"words": 176, "then_words": 110, "because_sentences": 1, "except": 0, "precedence": 1, "accepted": 1, "accepted_words": 36}, "C9": {"words": 298, "then_words": 219, "because_sentences": 1, "except": 0, "precedence": 1, "accepted": 1, "accepted_words": 48}, "C10": {"words": 377, "then_words": 225, "because_sentences": 1, "except": 1, "precedence": 1, "accepted": 5, "accepted_words": 311}, - "C11": {"words": 372, "then_words": 247, "because_sentences": 1, "except": 1, "precedence": 0, "accepted": 0, "accepted_words": 0}, + "C11": {"words": 391, "then_words": 226, "because_sentences": 1, "except": 2, "precedence": 0, "accepted": 0, "accepted_words": 0}, "C12": {"words": 316, "then_words": 107, "because_sentences": 2, "except": 2, "precedence": 1, "accepted": 2, "accepted_words": 59}, "C13": {"words": 259, "then_words": 74, "because_sentences": 1, "except": 1, "precedence": 1, "accepted": 0, "accepted_words": 0}, "C14": {"words": 231, "then_words": 111, "because_sentences": 3, "except": 1, "precedence": 0, "accepted": 0, "accepted_words": 0}, @@ -26,7 +26,7 @@ "H7": {"words": 343, "then_words": 274, "because_sentences": 2, "except": 0, "precedence": 0, "accepted": 1, "accepted_words": 54}, "H8": {"words": 431, "then_words": 68, "because_sentences": 3, "except": 3, "precedence": 3, "accepted": 3, "accepted_words": 128}, "M1": {"words": 231, "then_words": 120, "because_sentences": 2, "except": 1, "precedence": 2, "accepted": 0, "accepted_words": 0}, - "M2": {"words": 701, "then_words": 422, "because_sentences": 1, "except": 4, "precedence": 1, "accepted": 6, "accepted_words": 209}, + "M2": {"words": 714, "then_words": 435, "because_sentences": 1, "except": 4, "precedence": 1, "accepted": 6, "accepted_words": 209}, "M3": {"words": 234, "then_words": 84, "because_sentences": 2, "except": 2, "precedence": 2, "accepted": 1, "accepted_words": 80}, "M4": {"words": 327, "then_words": 164, "because_sentences": 2, "except": 1, "precedence": 1, "accepted": 0, "accepted_words": 0}, "N1": {"words": 95, "then_words": 62, "because_sentences": 1, "except": 0, "precedence": 0, "accepted": 0, "accepted_words": 0}, @@ -54,12 +54,12 @@ "R5": {"words": 250, "then_words": 154, "because_sentences": 2, "except": 0, "precedence": 0, "accepted": 0, "accepted_words": 0}, "S1": {"words": 274, "then_words": 26, "because_sentences": 1, "except": 2, "precedence": 2, "accepted": 0, "accepted_words": 0}, "S2": {"words": 657, "then_words": 119, "because_sentences": 1, "except": 0, "precedence": 7, "accepted": 3, "accepted_words": 214}, - "S3": {"words": 928, "then_words": 398, "because_sentences": 3, "except": 4, "precedence": 4, "accepted": 6, "accepted_words": 334}, - "S4": {"words": 298, "then_words": 156, "because_sentences": 2, "except": 0, "precedence": 3, "accepted": 0, "accepted_words": 0}, - "S5": {"words": 665, "then_words": 350, "because_sentences": 1, "except": 2, "precedence": 1, "accepted": 0, "accepted_words": 0}, - "S6": {"words": 419, "then_words": 203, "because_sentences": 2, "except": 0, "precedence": 2, "accepted": 0, "accepted_words": 0}, + "S3": {"words": 960, "then_words": 398, "because_sentences": 3, "except": 4, "precedence": 5, "accepted": 6, "accepted_words": 343}, + "S4": {"words": 306, "then_words": 164, "because_sentences": 2, "except": 0, "precedence": 3, "accepted": 0, "accepted_words": 0}, + "S5": {"words": 719, "then_words": 400, "because_sentences": 1, "except": 2, "precedence": 1, "accepted": 0, "accepted_words": 0}, + "S6": {"words": 580, "then_words": 325, "because_sentences": 2, "except": 1, "precedence": 2, "accepted": 0, "accepted_words": 0}, "S7": {"words": 602, "then_words": 388, "because_sentences": 1, "except": 2, "precedence": 2, "accepted": 1, "accepted_words": 105}, - "S8": {"words": 418, "then_words": 267, "because_sentences": 1, "except": 1, "precedence": 2, "accepted": 0, "accepted_words": 0}, + "S8": {"words": 467, "then_words": 302, "because_sentences": 1, "except": 1, "precedence": 2, "accepted": 1, "accepted_words": 60}, "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": 474, "then_words": 266, "because_sentences": 3, "except": 1, "precedence": 1, "accepted": 2, "accepted_words": 107}, diff --git a/tests/v2/test_cases.py b/tests/v2/test_cases.py index 217fa2f88..f13738a68 100644 --- a/tests/v2/test_cases.py +++ b/tests/v2/test_cases.py @@ -250,9 +250,15 @@ def test_the_field_sweep_sees_the_claims_it_checks() -> None: # 1529 -> 1568 the same day, #891 (phase 2e) merged over it: the # split-credential rows, two run rows and the particle-chain row # (9), the split credential's own report on nine rows (27) and the - # opening-credential row (3), each claim under the three orders. - assert claims == 1568, ( - f"the field sweep checks {claims} claims, recorded as 1568 on " + # opening-credential row (3), each claim under the three orders; + # 1568 -> 1310 the same day, #785 (phase 2g): a listed member its + # capitals settle in a name of mixed case reports nothing, which + # takes the claims of the rows that reported one (each under + # the orders it reaches); its new rows add their run reports, and + # the rows rewritten in one case or lower case to keep reaching + # their emitter add theirs back. + assert claims == 1310, ( + f"the field sweep checks {claims} claims, recorded as 1310 on " f"2026-10-25") diff --git a/tests/v2/test_contracts.py b/tests/v2/test_contracts.py index e00367c3b..0b2386c2f 100644 --- a/tests/v2/test_contracts.py +++ b/tests/v2/test_contracts.py @@ -15,7 +15,9 @@ AmbiguityKind.UNBALANCED_DELIMITER: 'Jon "Nick Smith', AmbiguityKind.COMMA_STRUCTURE: "Smith, John, Extra, Jr.", AmbiguityKind.SUFFIX_OR_NICKNAME: "JEFFREY (JD) BRICKEN", - AmbiguityKind.SUFFIX_OR_NAME: "John Smith MA", + # Title case: capitals in a name of mixed case settle the word and + # report nothing (rules/S6, #785) + AmbiguityKind.SUFFIX_OR_NAME: "John Smith Ma", AmbiguityKind.GIVEN_OR_FAMILY: "Andrew", AmbiguityKind.TITLE_OR_NAME: "Lord Chancellor", # no emitter yet -- arrives with locale-pack order detection (2.x) diff --git a/tests/v2/test_parser.py b/tests/v2/test_parser.py index c92861420..c17580fb1 100644 --- a/tests/v2/test_parser.py +++ b/tests/v2/test_parser.py @@ -196,8 +196,10 @@ def test_phd_split_mid_name_is_a_suffix() -> None: def test_ambiguous_acronym_reports_the_reading_it_took() -> None: # 'ma' is both a post-nominal and a surname, so whichever way the # peel resolves it is a guess -- the same shape as the leading - # ambiguous particle that already reports PARTICLE_OR_GIVEN - took_suffix = parse("John Smith MA") + # ambiguous particle that already reports PARTICLE_OR_GIVEN. In a + # name written in one case: in a name of mixed case the capitals + # settle the word and nothing reports it (rules/S6, #785) + took_suffix = parse("JOHN SMITH MA") assert took_suffix.suffix == "MA" assert [a.kind for a in took_suffix.ambiguities] == \ [AmbiguityKind.SUFFIX_OR_NAME] @@ -206,12 +208,18 @@ def test_ambiguous_acronym_reports_the_reading_it_took() -> None: # MOVED by #289, not deleted: 'MA' is written in capitals inside a # mixed-case name, so it now leans CREDENTIAL and is taken with no # words to spare -- 'Jack' is the only name word left, which is - # what also turns on the GIVEN_OR_FAMILY fork (decisions/S6). + # what also turns on the GIVEN_OR_FAMILY fork (decisions/S6). The + # capitals settled 'MA', so that fork is the only one (#785). took_family = parse("Jack MA") assert took_family.suffix == "MA" assert not took_family.family assert set(a.kind for a in took_family.ambiguities) == \ - {AmbiguityKind.SUFFIX_OR_NAME, AmbiguityKind.GIVEN_OR_FAMILY} + {AmbiguityKind.GIVEN_OR_FAMILY} + # and in one case the count decides, and reports the family it kept + took_family = parse("JACK MA") + assert took_family.family == "MA" + assert [a.kind for a in took_family.ambiguities] == \ + [AmbiguityKind.SUFFIX_OR_NAME] @pytest.mark.parametrize("text", [ @@ -219,6 +227,7 @@ def test_ambiguous_acronym_reports_the_reading_it_took() -> None: "Ma, Jack", # a comma fixes the family "Joao da Silva do Amaral de Souza", # 'do' mid-name, never at the peel "John Smith PhD", # unambiguous vocabulary + "John Smith MA", # capitals settle it (#785) ]) def test_no_suffix_ambiguity_when_nothing_was_guessed(text: str) -> None: assert [a for a in parse(text).ambiguities @@ -242,7 +251,8 @@ def test_delimited_ambiguous_acronym_reports_suffix_or_nickname() -> None: def test_every_ambiguous_acronym_in_a_name_is_reported() -> None: # one coin-flip per acronym: a single-slot record dropped all but # the last, which defeats the point of reporting at all - n = parse("John Smith MA JD") + # (in one case, where no writing settles either word, rules/S6) + n = parse("JOHN SMITH MA JD") # the scenery the record is read off; only its separator moved (#436) assert n.suffix == "MA JD" assert [a.kind for a in n.ambiguities] == \ @@ -946,7 +956,7 @@ def test_each_suffix_or_name_branch_describes_itself() -> None: # roman-numeral branch turns on the letter being a numeral. Sharing # one template made "V written without periods" -- a distinction # that does not exist for V -- and hid which branch fired. - acronym = parse("John Smith MA").ambiguities[0].detail + acronym = parse("JOHN SMITH MA").ambiguities[0].detail assert "periods" in acronym and "post-nominal" in acronym numeral = parse("John Smith V").ambiguities[0].detail diff --git a/tests/v2/test_properties.py b/tests/v2/test_properties.py index 6cc3cd472..fa9332f03 100644 --- a/tests/v2/test_properties.py +++ b/tests/v2/test_properties.py @@ -313,6 +313,16 @@ def test_the_comma_agreement_exceptions_are_all_still_exceptions( _MAIDEN_ANCHORED_HEAD_DIGEST = ( "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") _MAIDEN_AGREEMENT_EXCEPTIONS = 1026 +#: #785: pairs whose 'MA' body is no name word, its capitals settling +#: it as the credential in a name of mixed case (rules/S6). Where the +#: marker is an ordinary word -- after a family comma (rules/M2) -- +#: such a body is a credential written before the member, and starts +#: the credential's run (rules/S8), so removing the "clause" removes a +#: credential and changes the question, which the docstring's +#: name-words-only premise rules out. Recorded 2026-10-25: 288 pairs, +#: every one on a family-comma head ('Doe, Jane', 'Smith, Jane', +#: 'Doe, J.', 'Doe, Dr. Jane'). +_MAIDEN_SETTLED_BODY_PAIRS = 288 _MAIDEN_AGREEMENT_DIGEST = ( "4b70727a2633fea1a9c219173d48b223cc0866a5d340b69a9eb4f14fc386ae6a") @@ -622,6 +632,7 @@ def audit(label: str, text: str, name: ParsedName) -> None: for bad in _credential_without_suffix_role(name)) failures = [] + settled_body = 0 for label, policy in policies: parser = Parser(policy=policy) for head in heads: @@ -653,6 +664,11 @@ def audit(label: str, text: str, name: ParsedName) -> None: anchored_head.append( f"{label}|{marker}|{clause}") continue + if body == "MA" and next( + t for t in clause_name.tokens + if t.text == body).role is Role.SUFFIX: + settled_body += 1 + continue failures.append( f"[{label}] {clause!r} reads " f"{clause_side} but {plain!r} reads " @@ -671,6 +687,9 @@ def audit(label: str, text: str, name: ParsedName) -> None: assert not failures, ( f"{len(failures)} of {pairs} pair(s) disagree outside the " f"one-case-head class:\n" + "\n".join(failures[:15])) + assert settled_body == _MAIDEN_SETTLED_BODY_PAIRS, ( + f"{settled_body} pairs disagree on a body its capitals settled, " + f"recorded as {_MAIDEN_SETTLED_BODY_PAIRS} on 2026-10-25") anchored_digest = hashlib.sha256( "\n".join(sorted(anchored_head)).encode()).hexdigest() assert (len(anchored_head), anchored_digest) == ( @@ -703,7 +722,10 @@ def audit(label: str, text: str, name: ParsedName) -> None: #: report and an assign-peel report on the same parse, each naming a #: different token. Written out rather than generated, because what #: makes them work is the CONTRAST between the two members' writing. -_TAILS = ("Ma JD", "MA JD", "MA Ma") +#: The lower-case pair joined them in #785: capitals in a name of mixed +#: case settle a member and report nothing (rules/S6), and 'Ma jd' +#: keeps the shape with two members no writing settles. +_TAILS = ("Ma JD", "MA JD", "MA Ma", "Ma jd") def _maiden_clause_grid() -> list[tuple[str, Parser, str]]: @@ -986,7 +1008,10 @@ def test_no_two_ambiguities_name_the_same_token_span() -> None: either kept by the clause or peeled by assign and no grid row had TWO. The `_TAILS` below are that shape: 'Ma JD' keeps 'Ma' and peels 'JD', 'MA JD' releases 'MA' and peels 'JD' behind it, and - 'MA Ma' keeps the Title-case one with the caps one in front. The + 'MA Ma' keeps the Title-case one with the caps one in front. Since + #785 a capitals member in a name of mixed case is settled and + reports nothing (rules/S6), so 'Ma jd' carries the two-report + shape where the capitals tails no longer do. The count is re-pinned on the widened grid, and the pairing is asserted directly beside it so a later edit that drops the tails fails here rather than silently turning this back into a test of @@ -1064,10 +1089,14 @@ def test_no_two_ambiguities_name_the_same_token_span() -> None: # P6's -- and 1,965 arrived, 1,134 on 'Jane Doe Jr.', whose # credential now starts a run that takes the clause in and reports # what it absorbs, the rest on heads whose marker is an ordinary - # word now; the tails loop below adds 6. - assert multi == 4239, ( + # word now; the tails loop below adds 6. 4,239 -> 3,744 on + # 2026-10-25 (#785): a listed member its capitals settle in a name + # of mixed case reports nothing (rules/S6), so the grid's capitals + # spellings carry one report fewer (2,988 on the old grid), and the + # 'Ma jd' tail added beside them restores the two-report shape. + assert multi == 3744, ( f"{multi} of these parses carry more than one report, recorded " - f"as 4239 on 2026-10-04. A parse with one report cannot fail " + f"as 3744 on 2026-10-25. A parse with one report cannot fail " f"the check above, so this is what keeps the grid honest: move " f"the number deliberately, and never to 0") # 2,052 since 2026-10-04 (#601/#602), from 1,944: 216 parses left, @@ -1078,10 +1107,14 @@ def test_no_two_ambiguities_name_the_same_token_span() -> None: # clause-free view -- and on 'Jane Doe Jr.', whose credential run # reports what it absorbs. The run's reports moved from assign's # peel to the take that consumes the run (#601), so the pair is - # the maiden take's member report against the run's. - assert both == 2052, ( + # the maiden take's member report against the run's. 2,052 -> + # 1,728 on 2026-10-25 (#785): the capitals 'JD' and 'MA' of the + # first three tails are settled and report nothing (rules/S6), and + # the 'Ma jd' tail, whose members no writing settles, brings the + # pair back. + assert both == 1728, ( f"{both} parse(s) carry TWO suffix-or-name reports, recorded " - f"as 2052 on 2026-10-04. This is the pair the test is named " + f"as 1728 on 2026-10-25. This is the pair the test is named " f"for -- the clause's kept member against the trailing run's " f"reading -- and it was 0 for the whole grid until the " f"two-member tails were added. Never re-record it as 0") diff --git a/tools/differential/corpus_rules.jsonl b/tools/differential/corpus_rules.jsonl index effee77bf..2811f5611 100644 --- a/tools/differential/corpus_rules.jsonl +++ b/tools/differential/corpus_rules.jsonl @@ -8,6 +8,7 @@ ", Dr. fils" ", Jane" ", Jane Mrs" +", MA" ", MD/PhD" ".," "1. née Smith" @@ -77,6 +78,7 @@ "DOE, JANE MA VD PHD" "DOE, JANE PHD VD" "DOE, JANE PHD VD MA" +"DOE, JOHN MA JD" "DOE, JOHN Q MR" "DR. NÉE JUAN y LOPEZ" "DUT.RA MARIA SILVA" @@ -108,16 +110,19 @@ "Doe, John King" "Doe, John MA" "Doe, John MA JD" -"Doe, John MA Ma" "Doe, John MA Smith" +"Doe, John MA do" "Doe, John Ma" +"Doe, John Ma Smith" "Doe, John Master" "Doe, John Mr Jr" "Doe, John Msc.Ed." "Doe, John X.Y.Z." +"Doe, John ma Ma" "Doe, John van DO" "Doe, John van Ma" "Doe, MA PhD" +"Doe, Ma PhD" "Doe, Mrs" "Doe, PhD Jones" "Doe, abdul Mrs" @@ -211,15 +216,18 @@ "J.R. Smith" "J.Smith" "JACK MA" +"JANE DOE MA R.T." "JOHN PROF. MA" "JOHN QUINCY SMITH I" "JOHN SMITH JR, AD" "JOHN SMITH MA" +"JOHN SMITH MA JONES" "JOHN SMITH NEE X X" "JOHN SMITH PH.D." "JOHN SMITH SAM E." "JOHN SMITH VI" "JOHN SMITH, FNP-BC" +"JOHN SMITH, MA" "JOHN SMITH, PU-PH" "JOHN SMITH, VD MA" "JOHN V" @@ -248,6 +256,7 @@ "Jane Doe Dr. Ph. D." "Jane Doe Jr. nee Smith" "Jane Doe MA R.T." +"Jane Doe Ma R.T." "Jane Doe Mrs" "Jane Doe PhD nee Smith" "Jane Doe de D Junior" @@ -262,6 +271,7 @@ "Jane Doe nee Smith MA" "Jane Doe nee Smith MA Prof." "Jane Doe nee Smith Ma" +"Jane Doe nee Smith Ma JD" "Jane Doe nee Smith PhD Smith" "Jane Doe nee Smith Prof." "Jane Doe nee Smith Prof. MA" @@ -326,6 +336,7 @@ "John Smith (MBA PhD.)" "John Smith (PhD MBA)" "John Smith 1.4" +"John Smith DO Jones" "John Smith Esq." "John Smith II, AD" "John Smith III" @@ -347,11 +358,14 @@ "John Smith K.C." "John Smith M. D." "John Smith M.A." +"John Smith MA" +"John Smith MA Jones" "John Smith MB ChB" "John Smith MD PhD" "John Smith MD-Jones" "John Smith MD-PhD" "John Smith Ma" +"John Smith Ma Jones" "John Smith Mc V" "John Smith Msc.Ed." "John Smith Ph. D. Jones" @@ -530,6 +544,8 @@ "Mai Le née Nguyen" "Malcolm X" "Mari' Aube'" +"Maria Ana M.A. Silva" +"Maria Ana MA Silva" "Maria Kowalska (z domu Nowak)" "Maria Kowalska (z domu)" "Maria Kowalska (z, domu Nowak)" @@ -675,6 +691,7 @@ "Smith, Jan van Prof." "Smith, Jane Mrs" "Smith, John" +"Smith, John MA Jones" "Smith, John MD/PhD" "Smith, John PSM I, Jr." "Smith, John PhD I, Jr." diff --git a/tools/differential/corpus_shapes.jsonl b/tools/differential/corpus_shapes.jsonl index ebcb14d3a..e2e68b237 100644 --- a/tools/differential/corpus_shapes.jsonl +++ b/tools/differential/corpus_shapes.jsonl @@ -33,13 +33,13 @@ {"name": "Jane Doe nee Puig i Soler", "shape": 1} {"name": "Jane Doe nee Smith 2", "shape": 1} {"name": "Jane Doe nee Smith MA", "shape": 1} -{"name": "Jane Doe nee Smith MA JD", "shape": 1} {"name": "Jane Doe nee Smith MA PhD", "shape": 1} {"name": "Jane Doe nee Smith MA Prof.", "shape": 1} {"name": "Jane Doe nee Smith MA y", "shape": 1} {"name": "Jane Doe nee Smith Ma", "shape": 1} {"name": "Jane Doe nee Smith Ma JD", "shape": 1} {"name": "Jane Doe nee Smith Ma Prof.", "shape": 1} +{"name": "Jane Doe nee Smith Ma y", "shape": 1} {"name": "Jane Doe nee Smith PhD MA", "shape": 1} {"name": "Jane Doe nee Smith PhD MEng", "shape": 1} {"name": "Jane Doe nee Smith Prof.", "shape": 1} @@ -48,6 +48,7 @@ {"name": "Jane Doe nee Smith V Prof.", "shape": 1} {"name": "Jane Doe nee Smith XYZ", "shape": 1} {"name": "Jane Doe nee Smith do", "shape": 1} +{"name": "Jane Doe nee Smith ma jd", "shape": 1} {"name": "Jane Doe nee Yo-Yo Ma", "shape": 1} {"name": "Jean DUPONT", "shape": 1} {"name": "Jean Pierre DUPONT", "shape": 1} @@ -143,6 +144,8 @@ {"name": "Berg, abdul nee Jones MA", "shape": 2} {"name": "Carod i Rovira, Josep", "shape": 2} {"name": "DOE, JOHN MA", "shape": 2} +{"name": "DOE, JOHN MA JD", "shape": 2} +{"name": "DOE, JOHN MA SMITH", "shape": 2} {"name": "DOE, MARY JO MA", "shape": 2} {"name": "De La Cruz, Ed", "shape": 2} {"name": "De La Cruz, M.J. K.L.", "shape": 2} @@ -165,6 +168,7 @@ {"name": "Doe, Jane nee Smith PhD MEng", "shape": 2} {"name": "Doe, Jane nee Smith do", "shape": 2} {"name": "Doe, Jane nee Smith ma", "shape": 2} +{"name": "Doe, Jane nee Smith ma do", "shape": 2} {"name": "Doe, John A.", "shape": 2} {"name": "Doe, John BA", "shape": 2} {"name": "Doe, John DO", "shape": 2} @@ -172,7 +176,6 @@ {"name": "Doe, John Do", "shape": 2} {"name": "Doe, John Ed", "shape": 2} {"name": "Doe, John MA", "shape": 2} -{"name": "Doe, John MA JD", "shape": 2} {"name": "Doe, John MA Jr", "shape": 2} {"name": "Doe, John MA Ma", "shape": 2} {"name": "Doe, John MA PhD", "shape": 2} @@ -184,6 +187,8 @@ {"name": "Doe, John PhD MA", "shape": 2} {"name": "Doe, John Q. MA", "shape": 2} {"name": "Doe, John X.Y.Z.", "shape": 2} +{"name": "Doe, John ma Ma", "shape": 2} +{"name": "Doe, John ma do", "shape": 2} {"name": "Doe, John van DO", "shape": 2} {"name": "Doe, John van Ma", "shape": 2} {"name": "Doe, MA", "shape": 2} @@ -196,7 +201,7 @@ {"name": "Jane Doe (nee Smith) MA", "shape": 2} {"name": "Jane Doe nee MA PhD", "shape": 2} {"name": "Jane Doe nee Smith DO DO", "shape": 2} -{"name": "Jane Doe nee Smith Ma JD", "shape": 2} +{"name": "Jane Doe nee Smith Ma jd", "shape": 2} {"name": "Jane Doe nee Smith V MA", "shape": 2} {"name": "John Smith, 1.4", "shape": 2} {"name": "John née Jones Smith Ma", "shape": 2} diff --git a/tools/differential/snapshot.jsonl b/tools/differential/snapshot.jsonl index 4074a4bfd..b407a2c71 100644 --- a/tools/differential/snapshot.jsonl +++ b/tools/differential/snapshot.jsonl @@ -14,6 +14,7 @@ {"name": ", Jane", "given": "Jane", "initials": "J."} {"name": ", Jane Mrs", "title": "Mrs", "family": "Jane", "initials": "J."} {"name": ", John", "given": "John", "initials": "J."} +{"name": ", MA", "given": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "M.", "capitalized": "Ma"} {"name": ", MD/PhD", "suffix": "MD/PhD"} {"name": ".,"} {"name": "1 & 2, 3 4 5, Mr.", "title": "Mr.", "given": "3", "middle": "4 5", "family": "1 & 2", "initials": "3. 4. 5. 1. 2."} @@ -351,13 +352,13 @@ {"name": "Berg, Jan van der", "given": "Jan", "family": "van der Berg", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "J. B."} {"name": "Berg, Jan van; MD", "given": "Jan", "middle": "van;", "family": "Berg", "suffix": "MD", "initials": "J. v. B."} {"name": "Berg, Jan vd", "given": "Jan", "family": "vd Berg", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. B."} -{"name": "Berg, Jane van der nee Smith DO", "given": "Jane", "middle": "van der nee Smith", "family": "Berg", "suffix": "DO", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. n. S. B.", "capitalized_force": "Jane van der Nee Smith Berg DO"} +{"name": "Berg, Jane van der nee Smith DO", "given": "Jane", "middle": "van der nee Smith", "family": "Berg", "suffix": "DO", "initials": "J. n. S. B.", "capitalized_force": "Jane van der Nee Smith Berg DO"} {"name": "Berg, abd née Jones", "given": "abd", "middle": "née Jones", "family": "Berg", "initials": "a. n. J. B.", "capitalized_force": "Abd Née Jones Berg"} {"name": "Berg, abdul MA", "given": "abdul MA", "family": "Berg", "initials": "a. M. B.", "capitalized_force": "Abdul Ma Berg"} {"name": "Berg, abdul Prof.", "title": "Prof.", "given": "abdul", "family": "Berg", "initials": "a. B.", "capitalized_force": "Prof. Abdul Berg"} {"name": "Berg, abdul Prof. Ali", "given": "abdul Prof.", "middle": "Ali", "family": "Berg", "initials": "a. P. A. B.", "capitalized_force": "Abdul Prof. Ali Berg"} {"name": "Berg, abdul V", "given": "abdul V", "family": "Berg", "initials": "a. V. B.", "capitalized_force": "Abdul V Berg"} -{"name": "Berg, abdul nee Jones MA", "given": "abdul", "middle": "nee Jones", "family": "Berg", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "a. n. J. B.", "capitalized_force": "Abdul Nee Jones Berg MA"} +{"name": "Berg, abdul nee Jones MA", "given": "abdul", "middle": "nee Jones", "family": "Berg", "suffix": "MA", "initials": "a. n. J. B.", "capitalized_force": "Abdul Nee Jones Berg MA"} {"name": "Berg, abdul van", "given": "abdul van", "family": "Berg", "initials": "a. v. B.", "capitalized_force": "Abdul Van Berg"} {"name": "Berg, abdul vd", "given": "abdul", "family": "vd Berg", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "a. B.", "capitalized_force": "Abdul vd Berg"} {"name": "Berg, de; Smith", "given": "de;", "middle": "Smith", "family": "Berg", "initials": "d. S. B.", "capitalized_force": "De; Smith Berg"} @@ -557,6 +558,8 @@ {"name": "DOE, JANE PHD VD", "given": "JANE", "family": "VD DOE", "suffix": "PHD", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized": "Jane vd Doe PhD"} {"name": "DOE, JANE PHD VD MA", "given": "JANE", "family": "DOE", "suffix": "PHD VD MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized": "Jane Doe PhD VD MA"} {"name": "DOE, JOHN MA", "given": "JOHN", "family": "DOE", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized": "John Doe MA"} +{"name": "DOE, JOHN MA JD", "given": "JOHN", "family": "DOE", "suffix": "MA JD", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized": "John Doe MA JD"} +{"name": "DOE, JOHN MA SMITH", "given": "JOHN", "middle": "MA SMITH", "family": "DOE", "initials": "J. M. S. D.", "capitalized": "John Ma Smith Doe"} {"name": "DOE, JOHN Q MR", "title": "MR", "given": "JOHN", "middle": "Q", "family": "DOE", "initials": "J. Q. D.", "capitalized": "Mr John Q Doe"} {"name": "DOE, MARY JO MA", "given": "MARY", "middle": "JO", "family": "DOE", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "M. J. D.", "capitalized": "Mary Jo Doe MA"} {"name": "DR DOE", "title": "DR", "family": "DOE", "initials": "D.", "capitalized": "Dr Doe"} @@ -627,13 +630,13 @@ {"name": "Doe, Dr. John A. Kenneth III", "title": "Dr.", "given": "John", "middle": "A. Kenneth", "family": "Doe", "suffix": "III", "initials": "J. A. K. D."} {"name": "Doe, Dr. John A. Kenneth Jr.", "title": "Dr.", "given": "John", "middle": "A. Kenneth", "family": "Doe", "suffix": "Jr.", "initials": "J. A. K. D."} {"name": "Doe, Dr. John III", "title": "Dr.", "given": "John", "family": "Doe", "suffix": "III", "initials": "J. D."} -{"name": "Doe, Dr. John MA", "title": "Dr.", "given": "John", "family": "Doe", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} +{"name": "Doe, Dr. John MA", "title": "Dr.", "given": "John", "family": "Doe", "suffix": "MA", "initials": "J. D."} {"name": "Doe, Dr. John P., CLU, CFP, LUTC", "title": "Dr.", "given": "John", "middle": "P.", "family": "Doe", "suffix": "CLU, CFP, LUTC", "ambiguities": ["COMMA_STRUCTURE"], "initials": "J. P. D.", "capitalized_force": "Dr. John P. Doe CLU, CFP, Lutc"} {"name": "Doe, Dr. John, Jr.", "title": "Dr.", "given": "John", "family": "Doe", "suffix": "Jr.", "initials": "J. D."} {"name": "Doe, Dr. MA", "title": "Dr.", "family": "Doe", "suffix": "MA", "initials": "D."} {"name": "Doe, Dr. MA Smith", "title": "Dr.", "given": "MA", "middle": "Smith", "family": "Doe", "initials": "M. S. D.", "capitalized_force": "Dr. Ma Smith Doe"} -{"name": "Doe, Dr. nee Smith MA", "title": "Dr.", "given": "nee", "middle": "Smith", "family": "Doe", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "n. S. D.", "capitalized_force": "Dr. Nee Smith Doe MA"} -{"name": "Doe, J. MA", "given": "J.", "family": "Doe", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} +{"name": "Doe, Dr. nee Smith MA", "title": "Dr.", "given": "nee", "middle": "Smith", "family": "Doe", "suffix": "MA", "initials": "n. S. D.", "capitalized_force": "Dr. Nee Smith Doe MA"} +{"name": "Doe, J. MA", "given": "J.", "family": "Doe", "suffix": "MA", "initials": "J. D."} {"name": "Doe, J. ba", "given": "J.", "family": "Doe", "suffix": "ba", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized_force": "J. Doe BA"} {"name": "Doe, J. nee MA ba", "given": "J.", "middle": "nee", "family": "Doe", "suffix": "MA ba", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. n. D.", "capitalized_force": "J. Nee Doe MA BA"} {"name": "Doe, Jane Dr", "given": "Jane", "middle": "Dr", "family": "Doe", "initials": "J. D. D."} @@ -645,18 +648,19 @@ {"name": "Doe, Jane Mrs, PhD", "title": "Mrs", "given": "Jane", "family": "Doe", "suffix": "PhD", "initials": "J. D."} {"name": "Doe, Jane Ms", "given": "Jane", "family": "Doe", "suffix": "Ms", "initials": "J. D.", "capitalized_force": "Jane Doe MS"} {"name": "Doe, Jane PhD MEng", "given": "Jane", "family": "Doe", "suffix": "PhD MEng", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized_force": "Jane Doe PhD MENG"} -{"name": "Doe, Jane Q. nee Smith MA", "given": "Jane", "middle": "Q. nee Smith", "family": "Doe", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. Q. n. S. D.", "capitalized_force": "Jane Q. Nee Smith Doe MA"} +{"name": "Doe, Jane Q. nee Smith MA", "given": "Jane", "middle": "Q. nee Smith", "family": "Doe", "suffix": "MA", "initials": "J. Q. n. S. D.", "capitalized_force": "Jane Q. Nee Smith Doe MA"} {"name": "Doe, Jane nee Puig i Soler", "given": "Jane", "middle": "nee Puig i Soler", "family": "Doe", "initials": "J. n. P. S. D.", "capitalized_force": "Jane Nee Puig i Soler Doe"} {"name": "Doe, Jane nee Smith", "given": "Jane", "middle": "nee Smith", "family": "Doe", "initials": "J. n. S. D.", "capitalized_force": "Jane Nee Smith Doe"} -{"name": "Doe, Jane nee Smith DO", "given": "Jane", "middle": "nee Smith", "family": "Doe", "suffix": "DO", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. n. S. D.", "capitalized_force": "Jane Nee Smith Doe DO"} +{"name": "Doe, Jane nee Smith DO", "given": "Jane", "middle": "nee Smith", "family": "Doe", "suffix": "DO", "initials": "J. n. S. D.", "capitalized_force": "Jane Nee Smith Doe DO"} {"name": "Doe, Jane nee Smith Do", "given": "Jane", "middle": "nee Smith", "family": "Do Doe", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "J. n. S. D.", "capitalized_force": "Jane Nee Smith do Doe"} -{"name": "Doe, Jane nee Smith MA", "given": "Jane", "middle": "nee Smith", "family": "Doe", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. n. S. D.", "capitalized_force": "Jane Nee Smith Doe MA"} -{"name": "Doe, Jane nee Smith MA Prof.", "title": "Prof.", "given": "Jane", "middle": "nee Smith", "family": "Doe", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. n. S. D.", "capitalized_force": "Prof. Jane Nee Smith Doe MA"} -{"name": "Doe, Jane nee Smith MA do", "given": "Jane", "middle": "nee Smith MA", "family": "do Doe", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "J. n. S. M. D.", "capitalized_force": "Jane Nee Smith Ma do Doe"} +{"name": "Doe, Jane nee Smith MA", "given": "Jane", "middle": "nee Smith", "family": "Doe", "suffix": "MA", "initials": "J. n. S. D.", "capitalized_force": "Jane Nee Smith Doe MA"} +{"name": "Doe, Jane nee Smith MA Prof.", "title": "Prof.", "given": "Jane", "middle": "nee Smith", "family": "Doe", "suffix": "MA", "initials": "J. n. S. D.", "capitalized_force": "Prof. Jane Nee Smith Doe MA"} +{"name": "Doe, Jane nee Smith MA do", "given": "Jane", "middle": "nee Smith", "family": "Doe", "suffix": "MA do", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. n. S. D.", "capitalized_force": "Jane Nee Smith Doe MA DO"} {"name": "Doe, Jane nee Smith Ma", "given": "Jane", "middle": "nee Smith Ma", "family": "Doe", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. n. S. M. D.", "capitalized_force": "Jane Nee Smith Ma Doe"} {"name": "Doe, Jane nee Smith PhD MEng", "given": "Jane", "middle": "nee Smith", "family": "Doe", "suffix": "PhD MEng", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. n. S. D.", "capitalized_force": "Jane Nee Smith Doe PhD MENG"} {"name": "Doe, Jane nee Smith do", "given": "Jane", "middle": "nee Smith", "family": "do Doe", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "J. n. S. D.", "capitalized_force": "Jane Nee Smith do Doe"} {"name": "Doe, Jane nee Smith ma", "given": "Jane", "middle": "nee Smith", "family": "Doe", "suffix": "ma", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. n. S. D.", "capitalized_force": "Jane Nee Smith Doe MA"} +{"name": "Doe, Jane nee Smith ma do", "given": "Jane", "middle": "nee Smith ma", "family": "do Doe", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "J. n. S. m. D.", "capitalized_force": "Jane Nee Smith Ma do Doe"} {"name": "Doe, Jane van Mrs", "title": "Mrs", "given": "Jane", "family": "van Doe", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "J. D."} {"name": "Doe, Jane, and Jr.", "given": "Jane", "family": "Doe", "suffix": "and Jr.", "ambiguities": ["COMMA_STRUCTURE"], "initials": "J. D."} {"name": "Doe, John", "given": "John", "family": "Doe", "initials": "J. D."} @@ -667,8 +671,8 @@ {"name": "Doe, John A. Kenneth, Jr.", "given": "John", "middle": "A. Kenneth", "family": "Doe", "suffix": "Jr.", "initials": "J. A. K. D."} {"name": "Doe, John A., III", "given": "John", "middle": "A.", "family": "Doe", "suffix": "III", "initials": "J. A. D."} {"name": "Doe, John A., Jr.", "given": "John", "middle": "A.", "family": "Doe", "suffix": "Jr.", "initials": "J. A. D."} -{"name": "Doe, John BA", "given": "John", "family": "Doe", "suffix": "BA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} -{"name": "Doe, John DO", "given": "John", "family": "Doe", "suffix": "DO", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} +{"name": "Doe, John BA", "given": "John", "family": "Doe", "suffix": "BA", "initials": "J. D."} +{"name": "Doe, John DO", "given": "John", "family": "Doe", "suffix": "DO", "initials": "J. D."} {"name": "Doe, John DO Ed", "given": "John", "middle": "DO Ed", "family": "Doe", "initials": "J. E. D.", "capitalized_force": "John do Ed Doe"} {"name": "Doe, John Do", "given": "John", "family": "Do Doe", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "J. D.", "capitalized_force": "John do Doe"} {"name": "Doe, John Ed", "given": "John", "middle": "Ed", "family": "Doe", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. E. D."} @@ -676,22 +680,25 @@ {"name": "Doe, John III", "given": "John", "family": "Doe", "suffix": "III", "initials": "J. D."} {"name": "Doe, John Jr Mr", "title": "Mr", "given": "John", "family": "Doe", "suffix": "Jr", "initials": "J. D."} {"name": "Doe, John King", "given": "John", "middle": "King", "family": "Doe", "initials": "J. K. D."} -{"name": "Doe, John MA", "given": "John", "family": "Doe", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} -{"name": "Doe, John MA JD", "given": "John", "family": "Doe", "suffix": "MA JD", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} -{"name": "Doe, John MA Jr", "given": "John", "family": "Doe", "suffix": "MA Jr", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} -{"name": "Doe, John MA Ma", "given": "John", "middle": "MA Ma", "family": "Doe", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. M. M. D.", "capitalized_force": "John Ma Ma Doe"} -{"name": "Doe, John MA PhD", "given": "John", "family": "Doe", "suffix": "MA PhD", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} -{"name": "Doe, John MA Smith", "given": "John", "middle": "MA Smith", "family": "Doe", "initials": "J. M. S. D.", "capitalized_force": "John Ma Smith Doe"} -{"name": "Doe, John MA do", "given": "John", "middle": "MA", "family": "do Doe", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "J. M. D.", "capitalized_force": "John Ma do Doe"} +{"name": "Doe, John MA", "given": "John", "family": "Doe", "suffix": "MA", "initials": "J. D."} +{"name": "Doe, John MA JD", "given": "John", "family": "Doe", "suffix": "MA JD", "initials": "J. D."} +{"name": "Doe, John MA Jr", "given": "John", "family": "Doe", "suffix": "MA Jr", "initials": "J. D."} +{"name": "Doe, John MA Ma", "given": "John", "family": "Doe", "suffix": "MA Ma", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized_force": "John Doe MA MA"} +{"name": "Doe, John MA PhD", "given": "John", "family": "Doe", "suffix": "MA PhD", "initials": "J. D."} +{"name": "Doe, John MA Smith", "given": "John", "family": "Doe", "suffix": "MA Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} +{"name": "Doe, John MA do", "given": "John", "family": "Doe", "suffix": "MA do", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized_force": "John Doe MA DO"} {"name": "Doe, John MA y", "given": "John", "middle": "MA y", "family": "Doe", "initials": "J. M. D.", "capitalized_force": "John Ma y Doe"} {"name": "Doe, John Ma", "given": "John", "middle": "Ma", "family": "Doe", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. M. D."} +{"name": "Doe, John Ma Smith", "given": "John", "middle": "Ma Smith", "family": "Doe", "initials": "J. M. S. D."} {"name": "Doe, John Master", "given": "John", "middle": "Master", "family": "Doe", "initials": "J. M. D."} {"name": "Doe, John Mr Jr", "title": "Mr", "given": "John", "family": "Doe", "suffix": "Jr", "initials": "J. D."} {"name": "Doe, John Msc.Ed.", "given": "John", "family": "Doe", "suffix": "Msc.Ed.", "initials": "J. D."} -{"name": "Doe, John PhD MA", "given": "John", "family": "Doe", "suffix": "PhD MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} -{"name": "Doe, John Q. MA", "given": "John", "middle": "Q.", "family": "Doe", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. Q. D."} +{"name": "Doe, John PhD MA", "given": "John", "family": "Doe", "suffix": "PhD MA", "initials": "J. D."} +{"name": "Doe, John Q. MA", "given": "John", "middle": "Q.", "family": "Doe", "suffix": "MA", "initials": "J. Q. D."} {"name": "Doe, John X.Y.Z.", "given": "John", "family": "Doe", "suffix": "X.Y.Z.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} {"name": "Doe, John jr., MD", "given": "John", "family": "Doe", "suffix": "jr., MD", "initials": "J. D.", "capitalized_force": "John Doe Jr., MD"} +{"name": "Doe, John ma Ma", "given": "John", "middle": "ma Ma", "family": "Doe", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. m. M. D.", "capitalized_force": "John Ma Ma Doe"} +{"name": "Doe, John ma do", "given": "John", "middle": "ma", "family": "do Doe", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "J. m. D.", "capitalized_force": "John Ma do Doe"} {"name": "Doe, John van DO", "given": "John", "family": "van DO Doe", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "J. D.", "capitalized_force": "John van do Doe"} {"name": "Doe, John van Ma", "given": "John", "middle": "van Ma", "family": "Doe", "initials": "J. M. D."} {"name": "Doe, John, Jr.", "given": "John", "family": "Doe", "suffix": "Jr.", "initials": "J. D."} @@ -703,9 +710,10 @@ {"name": "Doe, John. A. Kenneth, Jr.", "title": "John.", "given": "A.", "middle": "Kenneth", "family": "Doe", "suffix": "Jr.", "initials": "A. K. D."} {"name": "Doe, Lt. Gen. John A. Kenneth IV", "title": "Lt. Gen.", "given": "John", "middle": "A. Kenneth", "family": "Doe", "suffix": "IV", "initials": "J. A. K. D."} {"name": "Doe, Lt.Gov. John", "title": "Lt.Gov.", "given": "John", "family": "Doe", "initials": "J. D."} -{"name": "Doe, MA", "family": "Doe", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "D."} -{"name": "Doe, MA PhD", "family": "Doe", "suffix": "MA PhD", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "D."} +{"name": "Doe, MA", "family": "Doe", "suffix": "MA", "initials": "D."} +{"name": "Doe, MA PhD", "family": "Doe", "suffix": "MA PhD", "initials": "D."} {"name": "Doe, MA Smith", "given": "MA", "middle": "Smith", "family": "Doe", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "M. S. D.", "capitalized_force": "Ma Smith Doe"} +{"name": "Doe, Ma PhD", "given": "Ma", "family": "Doe", "suffix": "PhD", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "M. D."} {"name": "Doe, Mary - Kate, RN", "given": "Mary", "middle": "- Kate", "family": "Doe", "suffix": "RN", "initials": "M. -. K. D."} {"name": "Doe, Mary Jo Ma", "given": "Mary", "middle": "Jo Ma", "family": "Doe", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "M. J. M. D."} {"name": "Doe, Mr. MA PhD", "title": "Mr.", "family": "Doe", "suffix": "MA PhD", "initials": "D."} @@ -734,7 +742,7 @@ {"name": "Dr Smith V", "title": "Dr", "given": "Smith", "family": "V", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "S. V."} {"name": "Dr 田中さん, V.", "title": "Dr", "family": "田中", "suffix": "さん, V.", "initials": "田."} {"name": "Dr 김민준, Jr.", "title": "Dr", "given": "민준", "family": "김", "suffix": "Jr.", "initials": "민. 김."} -{"name": "Dr 김민준, MA", "title": "Dr", "given": "민준", "family": "김", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "민. 김."} +{"name": "Dr 김민준, MA", "title": "Dr", "given": "민준", "family": "김", "suffix": "MA", "initials": "민. 김."} {"name": "Dr 김민준씨, Jr.", "title": "Dr", "given": "민준", "family": "김", "suffix": "씨, Jr.", "initials": "민. 김."} {"name": "Dr 김민준씨, V.", "title": "Dr", "given": "민준", "family": "김", "suffix": "씨, V.", "initials": "민. 김."} {"name": "Dr.", "title": "Dr."} @@ -809,7 +817,7 @@ {"name": "Dr. med. univ. Margit Popp", "title": "Dr. med. univ.", "given": "Margit", "family": "Popp", "initials": "M. P.", "capitalized_force": "Dr. Med. Univ. Margit Popp"} {"name": "Dr. med. univ. Margit Popp, MSc", "title": "Dr. med. univ.", "given": "Margit", "family": "Popp", "suffix": "MSc", "initials": "M. P.", "capitalized_force": "Dr. Med. Univ. Margit Popp MSc"} {"name": "Dr. nee Smith PhD Prof.", "title": "Dr. Prof.", "given": "nee", "family": "Smith", "suffix": "PhD", "initials": "n. S.", "capitalized_force": "Dr. Prof. Nee Smith PhD"} -{"name": "Dr. 김민준씨, MA", "title": "Dr.", "given": "민준", "family": "김", "suffix": "씨, MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "민. 김."} +{"name": "Dr. 김민준씨, MA", "title": "Dr.", "given": "민준", "family": "김", "suffix": "씨, MA", "initials": "민. 김."} {"name": "Dr., John", "given": "John", "family": "Dr.", "initials": "J. D."} {"name": "Dr.philol. Laima Kristīne Eva Sofija van den Volfa Bērziņa-Kalniņa", "title": "Dr.philol.", "given": "Laima", "middle": "Kristīne Eva Sofija", "family": "van den Volfa Bērziņa-Kalniņa", "initials": "L. K. E. S. V. B.", "capitalized_force": "Dr.Philol. Laima Kristīne Eva Sofija van den Volfa Bērziņa-Kalniņa"} {"name": "Dra. Andréia da Silva", "title": "Dra.", "given": "Andréia", "family": "da Silva", "initials": "A. S."} @@ -998,7 +1006,7 @@ {"name": "Frederic Bosch i Serra", "given": "Frederic", "family": "Bosch i Serra", "initials": "F. B. S."} {"name": "Frederic Viñals i Iscla", "given": "Frederic", "family": "Viñals i Iscla", "initials": "F. V. I."} {"name": "Freiherr St John Smith", "title": "Freiherr St", "given": "John", "family": "Smith", "initials": "J. S."} -{"name": "Freiherr von Berg MA", "title": "Freiherr", "family": "von Berg", "suffix": "MA", "ambiguities": ["PARTICLE_OR_GIVEN", "SUFFIX_OR_NAME"], "initials": "B."} +{"name": "Freiherr von Berg MA", "title": "Freiherr", "family": "von Berg", "suffix": "MA", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "B."} {"name": "Freiherr von Berg, Ed", "given": "Ed", "family": "Freiherr von Berg", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "E. B.", "capitalized_force": "Ed freiherr von Berg"} {"name": "Freiherr von Richthofen", "title": "Freiherr", "family": "von Richthofen", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "R."} {"name": "Freiherr von Richthofen V", "title": "Freiherr", "family": "von Richthofen V", "ambiguities": ["PARTICLE_OR_GIVEN", "SUFFIX_OR_NAME"], "initials": "R. V."} @@ -1330,6 +1338,7 @@ {"name": "J.씨", "given": "J.", "suffix": "씨", "initials": "J."} {"name": "JA KD", "given": "JA", "family": "KD", "initials": "J. K.", "capitalized": "Ja Kd"} {"name": "JACK MA", "given": "JACK", "family": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. M.", "capitalized": "Jack Ma"} +{"name": "JANE DOE MA R.T.", "given": "JANE", "middle": "DOE MA", "family": "R.T.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D. M. R.", "capitalized": "Jane Doe Ma R.T."} {"name": "JANE DOE NEE SMITH MA", "given": "JANE", "family": "DOE", "suffix": "MA", "maiden": "SMITH", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized": "Jane Doe (Smith) MA"} {"name": "JANE DOE NEE YO-YO MA", "given": "JANE", "family": "DOE", "suffix": "MA", "maiden": "YO-YO", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized": "Jane Doe (Yo-Yo) MA"} {"name": "JANE YO-YO MA", "given": "JANE", "family": "YO-YO", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. Y.", "capitalized": "Jane Yo-Yo MA"} @@ -1345,6 +1354,7 @@ {"name": "JOHN SMITH", "given": "JOHN", "family": "SMITH", "initials": "J. S.", "capitalized": "John Smith"} {"name": "JOHN SMITH JR, AD", "given": "AD", "family": "JOHN SMITH", "suffix": "JR", "initials": "A. J. S.", "capitalized": "Ad John Smith Jr"} {"name": "JOHN SMITH MA", "given": "JOHN", "family": "SMITH", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S.", "capitalized": "John Smith MA"} +{"name": "JOHN SMITH MA JONES", "given": "JOHN", "middle": "SMITH MA", "family": "JONES", "initials": "J. S. M. J.", "capitalized": "John Smith Ma Jones"} {"name": "JOHN SMITH NEE X X", "given": "JOHN", "family": "SMITH", "maiden": "X X", "initials": "J. S.", "capitalized": "John Smith (X X)"} {"name": "JOHN SMITH PH.D.", "given": "JOHN", "family": "SMITH", "suffix": "PH.D.", "initials": "J. S.", "capitalized": "John Smith Ph.D."} {"name": "JOHN SMITH SAM E.", "given": "JOHN", "family": "SMITH", "suffix": "SAM E.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S.", "capitalized": "John Smith SAM E."} @@ -1371,8 +1381,8 @@ {"name": "Jack I-V", "given": "Jack", "family": "I-V", "initials": "J. I."} {"name": "Jack I.-V.", "given": "Jack", "family": "I.-V.", "initials": "J. I."} {"name": "Jack M.A.", "given": "Jack", "suffix": "M.A.", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "J."} -{"name": "Jack MA", "given": "Jack", "suffix": "MA", "ambiguities": ["GIVEN_OR_FAMILY", "SUFFIX_OR_NAME"], "initials": "J."} -{"name": "Jack MA.", "given": "Jack", "suffix": "MA.", "ambiguities": ["GIVEN_OR_FAMILY", "SUFFIX_OR_NAME"], "initials": "J."} +{"name": "Jack MA", "given": "Jack", "suffix": "MA", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "J."} +{"name": "Jack MA.", "given": "Jack", "suffix": "MA.", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "J."} {"name": "Jack Ma", "given": "Jack", "family": "Ma", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. M."} {"name": "Jack Ma Jr", "given": "Jack", "family": "Ma", "suffix": "Jr", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. M."} {"name": "Jack Ma.", "given": "Jack", "family": "Ma.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. M."} @@ -1413,15 +1423,16 @@ {"name": "Jane Doe (nee Smith MA)", "given": "Jane", "family": "Doe", "maiden": "Smith MA", "initials": "J. D.", "capitalized_force": "Jane Doe (Smith Ma)"} {"name": "Jane Doe (nee Smith Ma)", "given": "Jane", "family": "Doe", "maiden": "Smith Ma", "initials": "J. D."} {"name": "Jane Doe (nee Smith Prof.)", "title": "Prof.", "given": "Jane", "family": "Doe", "maiden": "Smith", "initials": "J. D."} -{"name": "Jane Doe (nee Smith) MA", "given": "Jane", "family": "Doe", "suffix": "MA", "maiden": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} +{"name": "Jane Doe (nee Smith) MA", "given": "Jane", "family": "Doe", "suffix": "MA", "maiden": "Smith", "initials": "J. D."} {"name": "Jane Doe Dr. Ph. D.", "title": "Dr.", "given": "Jane", "family": "Doe", "suffix": "Ph. D.", "initials": "J. D."} {"name": "Jane Doe Jr. nee Smith", "given": "Jane", "family": "Doe", "suffix": "Jr. nee Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized_force": "Jane Doe Jr. Nee Smith"} {"name": "Jane Doe Jr. nee Smith Ma", "given": "Jane", "family": "Doe", "suffix": "Jr. nee Smith Ma", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized_force": "Jane Doe Jr. Nee Smith MA"} -{"name": "Jane Doe MA R.T.", "given": "Jane", "middle": "Doe MA", "family": "R.T.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D. M. R.", "capitalized_force": "Jane Doe Ma R.T."} +{"name": "Jane Doe MA R.T.", "given": "Jane", "family": "Doe", "suffix": "MA R.T.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} +{"name": "Jane Doe Ma R.T.", "given": "Jane", "middle": "Doe Ma", "family": "R.T.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D. M. R."} {"name": "Jane Doe Mrs", "given": "Jane", "middle": "Doe", "family": "Mrs", "initials": "J. D. M."} {"name": "Jane Doe PhD nee Smith", "given": "Jane", "family": "Doe", "suffix": "PhD nee Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized_force": "Jane Doe PhD Nee Smith"} {"name": "Jane Doe de D Junior", "given": "Jane", "middle": "Doe", "family": "de D", "suffix": "Junior", "initials": "J. D. D."} -{"name": "Jane Doe geb. Smith MA", "given": "Jane", "family": "Doe", "suffix": "MA", "maiden": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} +{"name": "Jane Doe geb. Smith MA", "given": "Jane", "family": "Doe", "suffix": "MA", "maiden": "Smith", "initials": "J. D."} {"name": "Jane Doe nee King.", "given": "Jane", "family": "Doe", "maiden": "King.", "initials": "J. D."} {"name": "Jane Doe nee MA", "given": "Jane", "family": "Doe", "maiden": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized_force": "Jane Doe (Ma)"} {"name": "Jane Doe nee MA PhD", "given": "Jane", "family": "Doe", "suffix": "PhD", "maiden": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized_force": "Jane Doe (Ma) PhD"} @@ -1432,20 +1443,21 @@ {"name": "Jane Doe nee Puig i Soler", "given": "Jane", "family": "Doe", "maiden": "Puig i Soler", "initials": "J. D."} {"name": "Jane Doe nee Smith 2", "given": "Jane", "family": "Doe", "suffix": "2", "maiden": "Smith", "initials": "J. D."} {"name": "Jane Doe nee Smith B.K.", "given": "Jane", "family": "Doe", "maiden": "Smith B.K.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} -{"name": "Jane Doe nee Smith DO DO", "given": "Jane", "family": "Doe", "suffix": "DO DO", "maiden": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} -{"name": "Jane Doe nee Smith MA", "given": "Jane", "family": "Doe", "suffix": "MA", "maiden": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} -{"name": "Jane Doe nee Smith MA JD", "given": "Jane", "family": "Doe", "suffix": "MA JD", "maiden": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} -{"name": "Jane Doe nee Smith MA PhD", "given": "Jane", "family": "Doe", "suffix": "MA PhD", "maiden": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} -{"name": "Jane Doe nee Smith MA Prof.", "title": "Prof.", "given": "Jane", "family": "Doe", "suffix": "MA", "maiden": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} -{"name": "Jane Doe nee Smith MA y", "given": "Jane", "family": "Doe", "maiden": "Smith MA y", "initials": "J. D.", "capitalized_force": "Jane Doe (Smith Ma y)"} +{"name": "Jane Doe nee Smith DO DO", "given": "Jane", "family": "Doe", "suffix": "DO DO", "maiden": "Smith", "initials": "J. D."} +{"name": "Jane Doe nee Smith MA", "given": "Jane", "family": "Doe", "suffix": "MA", "maiden": "Smith", "initials": "J. D."} +{"name": "Jane Doe nee Smith MA PhD", "given": "Jane", "family": "Doe", "suffix": "MA PhD", "maiden": "Smith", "initials": "J. D."} +{"name": "Jane Doe nee Smith MA Prof.", "title": "Prof.", "given": "Jane", "family": "Doe", "suffix": "MA", "maiden": "Smith", "initials": "J. D."} +{"name": "Jane Doe nee Smith MA y", "given": "Jane", "family": "Doe", "suffix": "MA y", "maiden": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} {"name": "Jane Doe nee Smith Ma", "given": "Jane", "family": "Doe", "maiden": "Smith Ma", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} {"name": "Jane Doe nee Smith Ma JD", "given": "Jane", "family": "Doe", "suffix": "JD", "maiden": "Smith Ma", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} {"name": "Jane Doe nee Smith Ma Prof.", "title": "Prof.", "given": "Jane", "family": "Doe", "maiden": "Smith Ma", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} -{"name": "Jane Doe nee Smith PhD MA", "given": "Jane", "family": "Doe", "suffix": "PhD MA", "maiden": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} +{"name": "Jane Doe nee Smith Ma jd", "given": "Jane", "family": "Doe", "suffix": "jd", "maiden": "Smith Ma", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized_force": "Jane Doe (Smith Ma) JD"} +{"name": "Jane Doe nee Smith Ma y", "given": "Jane", "family": "Doe", "maiden": "Smith Ma y", "initials": "J. D."} +{"name": "Jane Doe nee Smith PhD MA", "given": "Jane", "family": "Doe", "suffix": "PhD MA", "maiden": "Smith", "initials": "J. D."} {"name": "Jane Doe nee Smith PhD MEng", "given": "Jane", "family": "Doe", "suffix": "PhD MEng", "maiden": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized_force": "Jane Doe (Smith) PhD MENG"} {"name": "Jane Doe nee Smith PhD Smith", "given": "Jane", "family": "Doe", "suffix": "PhD Smith", "maiden": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} {"name": "Jane Doe nee Smith Prof.", "title": "Prof.", "given": "Jane", "family": "Doe", "maiden": "Smith", "initials": "J. D."} -{"name": "Jane Doe nee Smith Prof. MA", "title": "Prof.", "given": "Jane", "family": "Doe", "suffix": "MA", "maiden": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} +{"name": "Jane Doe nee Smith Prof. MA", "title": "Prof.", "given": "Jane", "family": "Doe", "suffix": "MA", "maiden": "Smith", "initials": "J. D."} {"name": "Jane Doe nee Smith V", "given": "Jane", "family": "Doe", "suffix": "V", "maiden": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} {"name": "Jane Doe nee Smith V MA", "given": "Jane", "family": "Doe", "suffix": "MA", "maiden": "Smith V", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} {"name": "Jane Doe nee Smith V PhD", "given": "Jane", "family": "Doe", "suffix": "PhD", "maiden": "Smith V", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} @@ -1455,6 +1467,7 @@ {"name": "Jane Doe nee Smith X.Y.Z.", "given": "Jane", "family": "Doe", "suffix": "X.Y.Z.", "maiden": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} {"name": "Jane Doe nee Smith XYZ", "given": "Jane", "family": "Doe", "maiden": "Smith XYZ", "initials": "J. D.", "capitalized_force": "Jane Doe (Smith Xyz)"} {"name": "Jane Doe nee Smith do", "given": "Jane", "family": "Doe", "suffix": "do", "maiden": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized_force": "Jane Doe (Smith) DO"} +{"name": "Jane Doe nee Smith ma jd", "given": "Jane", "family": "Doe", "suffix": "ma jd", "maiden": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized_force": "Jane Doe (Smith) MA JD"} {"name": "Jane Doe nee Yo-Yo Ma", "given": "Jane", "family": "Doe", "maiden": "Yo-Yo Ma", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} {"name": "Jane Doe née Jones Smith V", "given": "Jane", "family": "Doe", "suffix": "V", "maiden": "Jones Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D."} {"name": "Jane Doe, MS LAc", "given": "Jane", "family": "Doe", "suffix": "MS LAc", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. D.", "capitalized_force": "Jane Doe MS LAC"} @@ -1480,7 +1493,7 @@ {"name": "Jane née Jones Smith", "family": "Jane", "maiden": "Jones Smith", "initials": "J."} {"name": "Jane née Jr y Jones", "given": "Jane", "middle": "née", "family": "Jr y Jones", "initials": "J. n. J. J.", "capitalized_force": "Jane Née Jr y Jones"} {"name": "Jane née and Jones Smith", "family": "Jane", "maiden": "and Jones Smith", "initials": "J."} -{"name": "Jane van der Berg nee Prof. King. MA", "title": "King.", "given": "Jane", "family": "van der Berg", "suffix": "MA", "maiden": "Prof.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. B."} +{"name": "Jane van der Berg nee Prof. King. MA", "title": "King.", "given": "Jane", "family": "van der Berg", "suffix": "MA", "maiden": "Prof.", "initials": "J. B."} {"name": "Jane van der Berg nee Smith Prof.", "title": "Prof.", "given": "Jane", "family": "van der Berg", "maiden": "Smith", "initials": "J. B."} {"name": "Jane van der Berg née", "given": "Jane", "family": "van der Berg née", "initials": "J. B. n.", "capitalized_force": "Jane van der Berg Née"} {"name": "Jane van der Berg née Jones", "given": "Jane", "family": "van der Berg", "maiden": "Jones", "initials": "J. B."} @@ -1609,19 +1622,20 @@ {"name": "John Ph. D.", "given": "John", "suffix": "Ph. D.", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "J."} {"name": "John PhD Smith", "given": "John", "middle": "PhD", "family": "Smith", "initials": "J. P. S."} {"name": "John Pharm D", "given": "John", "middle": "Pharm", "family": "D", "initials": "J. P. D."} -{"name": "John Prof. MA", "title": "Prof.", "family": "John", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J."} +{"name": "John Prof. MA", "title": "Prof.", "family": "John", "suffix": "MA", "initials": "J."} {"name": "John Q. Smith", "given": "John", "middle": "Q.", "family": "Smith", "initials": "J. Q. S."} {"name": "John Quincy Smith i", "given": "John", "middle": "Quincy", "family": "Smith", "suffix": "i", "initials": "J. Q. S.", "capitalized_force": "John Quincy Smith I"} {"name": "John Smith", "given": "John", "family": "Smith", "initials": "J. S."} {"name": "John Smith (M.A.)", "given": "John", "family": "Smith", "suffix": "M.A.", "initials": "J. S."} {"name": "John Smith (MA PhD)", "given": "John", "family": "Smith", "nickname": "MA PhD", "ambiguities": ["SUFFIX_OR_NICKNAME"], "initials": "J. S.", "capitalized_force": "John \"Ma PhD\" Smith"} {"name": "John Smith (MA)", "given": "John", "family": "Smith", "nickname": "MA", "ambiguities": ["SUFFIX_OR_NICKNAME"], "initials": "J. S.", "capitalized_force": "John \"Ma\" Smith"} -{"name": "John Smith (MA.)", "given": "John", "family": "Smith", "suffix": "MA.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S."} +{"name": "John Smith (MA.)", "given": "John", "family": "Smith", "suffix": "MA.", "initials": "J. S."} {"name": "John Smith (MBA PhD.)", "given": "John", "family": "Smith", "suffix": "MBA PhD.", "initials": "J. S."} {"name": "John Smith (PhD MBA)", "given": "John", "family": "Smith", "nickname": "PhD MBA", "initials": "J. S.", "capitalized_force": "John \"PhD Mba\" Smith"} {"name": "John Smith 1.4", "given": "John", "middle": "Smith", "family": "1.4", "initials": "J. S. 1."} {"name": "John Smith B.Tech.", "given": "John", "family": "Smith", "suffix": "B.Tech.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S.", "capitalized_force": "John Smith B.TECH."} {"name": "John Smith C.H.A.", "given": "John", "family": "Smith", "suffix": "C.H.A.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S."} +{"name": "John Smith DO Jones", "given": "John", "middle": "Smith", "family": "DO Jones", "initials": "J. S. J.", "capitalized_force": "John Smith do Jones"} {"name": "John Smith Dr.", "title": "Dr.", "given": "John", "family": "Smith", "initials": "J. S."} {"name": "John Smith E.S.Q.", "given": "John", "family": "Smith", "suffix": "E.S.Q.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S."} {"name": "John Smith Esq", "given": "John", "family": "Smith", "suffix": "Esq", "initials": "J. S."} @@ -1650,7 +1664,8 @@ {"name": "John Smith K.C.", "given": "John", "middle": "Smith", "family": "K.C.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S. K."} {"name": "John Smith M. D.", "given": "John", "middle": "Smith M.", "family": "D.", "initials": "J. S. M. D."} {"name": "John Smith M.A.", "given": "John", "family": "Smith", "suffix": "M.A.", "initials": "J. S."} -{"name": "John Smith MA", "given": "John", "family": "Smith", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S."} +{"name": "John Smith MA", "given": "John", "family": "Smith", "suffix": "MA", "initials": "J. S."} +{"name": "John Smith MA Jones", "given": "John", "family": "Smith", "suffix": "MA Jones", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S."} {"name": "John Smith MB ChB", "given": "John", "family": "Smith", "suffix": "MB ChB", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S."} {"name": "John Smith MC", "given": "John", "family": "Smith", "suffix": "MC", "initials": "J. S."} {"name": "John Smith MD PhD", "given": "John", "family": "Smith", "suffix": "MD PhD", "initials": "J. S."} @@ -1658,6 +1673,7 @@ {"name": "John Smith MD-PhD", "given": "John", "family": "Smith", "suffix": "MD-PhD", "initials": "J. S."} {"name": "John Smith MEng PhD", "given": "John", "middle": "Smith", "family": "MEng", "suffix": "PhD", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S. M.", "capitalized_force": "John Smith Meng PhD"} {"name": "John Smith Ma", "given": "John", "middle": "Smith", "family": "Ma", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S. M."} +{"name": "John Smith Ma Jones", "given": "John", "middle": "Smith Ma", "family": "Jones", "initials": "J. S. M. J."} {"name": "John Smith Mc V", "given": "John", "family": "Smith", "suffix": "Mc V", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S.", "capitalized_force": "John Smith MC V"} {"name": "John Smith Mr.", "title": "Mr.", "given": "John", "family": "Smith", "initials": "J. S."} {"name": "John Smith Msc.Ed.", "given": "John", "family": "Smith", "suffix": "Msc.Ed.", "initials": "J. S."} @@ -1730,10 +1746,10 @@ {"name": "John Smith, M. D.", "given": "M.", "middle": "D.", "family": "John Smith", "initials": "M. D. J. S."} {"name": "John Smith, M.D./Ph.D.", "given": "John", "family": "Smith", "suffix": "M.D./Ph.D.", "initials": "J. S."} {"name": "John Smith, M.D.; Ph.D.", "given": "John", "family": "Smith", "suffix": "M.D.; Ph.D.", "initials": "J. S."} -{"name": "John Smith, MA", "given": "John", "family": "Smith", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S."} +{"name": "John Smith, MA", "given": "John", "family": "Smith", "suffix": "MA", "initials": "J. S."} {"name": "John Smith, MA/PHD", "given": "John", "family": "Smith", "suffix": "MA/PHD", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S.", "capitalized_force": "John Smith MA/PhD"} {"name": "John Smith, MA/PhD", "given": "MA/PhD", "family": "John Smith", "initials": "M. J. S.", "capitalized_force": "Ma/PhD John Smith"} -{"name": "John Smith, MA; PhD", "given": "John", "family": "Smith", "suffix": "MA; PhD", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S."} +{"name": "John Smith, MA; PhD", "given": "John", "family": "Smith", "suffix": "MA; PhD", "initials": "J. S."} {"name": "John Smith, MD ; PhD", "given": "John", "family": "Smith", "suffix": "MD, PhD", "initials": "J. S."} {"name": "John Smith, MD MEng", "given": "John", "family": "Smith", "suffix": "MD MEng", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S.", "capitalized_force": "John Smith MD MENG"} {"name": "John Smith, MD, Bart", "given": "John", "family": "Smith", "suffix": "MD, Bart", "ambiguities": ["COMMA_STRUCTURE"], "initials": "J. S."} @@ -1910,7 +1926,7 @@ {"name": "Jozef", "given": "Jozef", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "J."} {"name": "Jozef", "order": "FAMILY_FIRST", "family": "Jozef", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "J."} {"name": "João Pedro Silva", "given": "João", "middle": "Pedro", "family": "Silva", "initials": "J. P. S."} -{"name": "Jo김민준씨, DO", "family": "Jo김민준", "suffix": "씨, DO", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J."} +{"name": "Jo김민준씨, DO", "family": "Jo김민준", "suffix": "씨, DO", "initials": "J."} {"name": "Jr.", "title": "Jr."} {"name": "Jr. Ph. D.", "title": "Jr.", "suffix": "Ph. D."} {"name": "Jr. Van Johnson", "title": "Jr.", "given": "Van", "family": "Johnson", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "V. J."} @@ -2071,7 +2087,7 @@ {"name": "Kim, AE-RA", "family": "Kim", "suffix": "AE-RA", "initials": "K."} {"name": "Kim, Ae-Ra", "given": "Ae-Ra", "family": "Kim", "initials": "A. K."} {"name": "Kim, Jr.", "family": "Kim", "suffix": "Jr.", "initials": "K."} -{"name": "Kim김민준씨, MA", "family": "Kim김민준", "suffix": "씨, MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "K."} +{"name": "Kim김민준씨, MA", "family": "Kim김민준", "suffix": "씨, MA", "initials": "K."} {"name": "King Curtis", "title": "King", "given": "Curtis", "initials": "C."} {"name": "King Henry", "title": "King", "given": "Henry", "initials": "H."} {"name": "King John Alexander V", "title": "King", "given": "John", "family": "Alexander", "suffix": "V", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. A."} @@ -2269,7 +2285,7 @@ {"name": "Magnús Eiríksson", "given": "Magnús", "family": "Eiríksson", "initials": "M. E."} {"name": "Magnús Jónsson", "given": "Magnús", "family": "Jónsson", "initials": "M. J."} {"name": "Mahathir bin Mohamad", "given": "Mahathir", "family": "bin Mohamad", "initials": "M. M."} -{"name": "Maheen Farooqi, ACA", "given": "Maheen", "family": "Farooqi", "suffix": "ACA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "M. F."} +{"name": "Maheen Farooqi, ACA", "given": "Maheen", "family": "Farooqi", "suffix": "ACA", "initials": "M. F."} {"name": "Mahmud II.", "given": "Mahmud", "suffix": "II.", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "M."} {"name": "Mai Le née Nguyen", "given": "Mai", "family": "Le", "maiden": "Nguyen", "initials": "M. L."} {"name": "Maid Marion", "title": "Maid", "given": "Marion", "initials": "M."} @@ -2315,6 +2331,8 @@ {"name": "Mari-Ann Henri Roobert Tammik, parlamendiliige", "given": "parlamendiliige", "family": "Mari-Ann Henri Roobert Tammik", "initials": "p. M. H. R. T.", "capitalized_force": "Parlamendiliige Mari-Ann Henri Roobert Tammik"} {"name": "Maria", "given": "Maria", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "M."} {"name": "Maria", "order": "FAMILY_FIRST", "family": "Maria", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "M."} +{"name": "Maria Ana M.A. Silva", "given": "Maria", "family": "Ana", "suffix": "M.A. Silva", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "M. A."} +{"name": "Maria Ana MA Silva", "given": "Maria", "family": "Ana", "suffix": "MA Silva", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "M. A."} {"name": "Maria Firmina dos Reis", "given": "Maria", "middle": "Firmina", "family": "dos Reis", "initials": "M. F. R."} {"name": "Maria Hasani", "given": "Maria", "family": "Hasani", "initials": "M. H."} {"name": "Maria Helena Fernandes", "given": "Maria", "middle": "Helena", "family": "Fernandes", "initials": "M. H. F."} @@ -2323,7 +2341,7 @@ {"name": "Maria Kowalska (z domu)", "given": "Maria", "family": "Kowalska", "nickname": "z domu", "initials": "M. K.", "capitalized_force": "Maria \"Z Domu\" Kowalska"} {"name": "Maria Kowalska (z, domu Nowak)", "given": "Maria", "family": "Kowalska", "nickname": "z, domu Nowak", "initials": "M. K.", "capitalized_force": "Maria \"Z, Domu Nowak\" Kowalska"} {"name": "Maria Kowalska z domu Nowak", "given": "Maria", "family": "Kowalska", "maiden": "Nowak", "initials": "M. K."} -{"name": "Maria Kowalska z domu Nowak MA", "given": "Maria", "family": "Kowalska", "suffix": "MA", "maiden": "Nowak", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "M. K."} +{"name": "Maria Kowalska z domu Nowak MA", "given": "Maria", "family": "Kowalska", "suffix": "MA", "maiden": "Nowak", "initials": "M. K."} {"name": "Maria Luisa y de la Cruz", "given": "Maria", "middle": "Luisa y de", "family": "la Cruz", "initials": "M. L. C."} {"name": "Maria López née García Pérez", "given": "Maria", "family": "López", "maiden": "García Pérez", "initials": "M. L."} {"name": "Maria Popescu", "given": "Maria", "family": "Popescu", "initials": "M. P."} @@ -3048,7 +3066,7 @@ {"name": "Smith Jr.", "given": "Smith", "suffix": "Jr.", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "S."} {"name": "Smith Jr., A.B.", "given": "A.B.", "family": "Smith", "suffix": "Jr.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "A. S."} {"name": "Smith Jr., John", "given": "John", "family": "Smith", "suffix": "Jr.", "initials": "J. S."} -{"name": "Smith Jr., MA", "family": "Smith", "suffix": "Jr., MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "S."} +{"name": "Smith Jr., MA", "family": "Smith", "suffix": "Jr., MA", "initials": "S."} {"name": "Smith Jr., Ma", "given": "Ma", "family": "Smith", "suffix": "Jr.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "M. S."} {"name": "Smith Jr., Mr.", "title": "Mr.", "family": "Smith", "suffix": "Jr.", "initials": "S."} {"name": "Smith Juan Vega", "given": "Smith", "middle": "Juan", "family": "Vega", "initials": "S. J. V."} @@ -3062,7 +3080,7 @@ {"name": "Smith Vi, John", "given": "John", "family": "Smith Vi", "initials": "J. S. V."} {"name": "Smith and King", "given": "Smith and King", "ambiguities": ["TITLE_OR_NAME"], "initials": "S. K."} {"name": "Smith born", "given": "Smith", "family": "born", "initials": "S. b.", "capitalized_force": "Smith Born"} -{"name": "Smith nee Jones, Jane MA", "given": "Jane", "family": "Smith", "suffix": "MA", "maiden": "Jones", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S."} +{"name": "Smith nee Jones, Jane MA", "given": "Jane", "family": "Smith", "suffix": "MA", "maiden": "Jones", "initials": "J. S."} {"name": "Smith née", "given": "Smith", "family": "née", "initials": "S. n.", "capitalized_force": "Smith Née"} {"name": "Smith née Jones", "family": "Smith", "maiden": "Jones", "initials": "S."} {"name": "Smith née Jones PhD", "family": "Smith", "suffix": "PhD", "maiden": "Jones", "initials": "S."} @@ -3093,6 +3111,7 @@ {"name": "Smith, John E, III, Jr", "given": "John", "middle": "E", "family": "Smith", "suffix": "III, Jr", "initials": "J. E. S."} {"name": "Smith, John I", "given": "John", "family": "Smith", "suffix": "I", "initials": "J. S."} {"name": "Smith, John Jr.", "given": "John", "family": "Smith", "suffix": "Jr.", "initials": "J. S."} +{"name": "Smith, John MA Jones", "given": "John", "family": "Smith", "suffix": "MA Jones", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S."} {"name": "Smith, John MD/PhD", "given": "John", "family": "Smith", "suffix": "MD/PhD", "initials": "J. S."} {"name": "Smith, John MEng", "given": "John", "middle": "MEng", "family": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. M. S.", "capitalized_force": "John Meng Smith"} {"name": "Smith, John PSM I, Jr.", "given": "John", "middle": "I", "family": "Smith", "suffix": "PSM, Jr.", "initials": "J. I. S."} @@ -3130,7 +3149,7 @@ {"name": "Smith, Jr.", "family": "Smith", "suffix": "Jr.", "initials": "S."} {"name": "Smith, Jr., PhD", "family": "Smith", "suffix": "Jr., PhD", "initials": "S."} {"name": "Smith, LEED AP", "given": "LEED", "middle": "AP", "family": "Smith", "initials": "L. A. S.", "capitalized_force": "Leed Ap Smith"} -{"name": "Smith, MA", "family": "Smith", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "S."} +{"name": "Smith, MA", "family": "Smith", "suffix": "MA", "initials": "S."} {"name": "Smith, MD - PhD - FACS", "family": "Smith", "suffix": "MD - PhD - FACS", "initials": "S."} {"name": "Smith, MD ; PhD", "family": "Smith", "suffix": "MD ; PhD", "initials": "S."} {"name": "Smith, MD PhD", "family": "Smith", "suffix": "MD PhD", "initials": "S."} @@ -3684,7 +3703,7 @@ {"name": "john j v, PhD", "given": "john", "family": "j", "suffix": "v, PhD", "initials": "j. j.", "capitalized": "John J V, PhD"} {"name": "john smith", "given": "john", "family": "smith", "initials": "j. s.", "capitalized": "John Smith"} {"name": "john smith EdD", "given": "john", "family": "smith", "suffix": "EdD", "initials": "j. s.", "capitalized": "John Smith EdD", "capitalized_force": "John Smith EDD"} -{"name": "john smith MA", "given": "john", "family": "smith", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "j. s.", "capitalized": "John Smith MA"} +{"name": "john smith MA", "given": "john", "family": "smith", "suffix": "MA", "initials": "j. s.", "capitalized": "John Smith MA"} {"name": "john smith MEng", "given": "john", "middle": "smith", "family": "MEng", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "j. s. M.", "capitalized_force": "John Smith Meng"} {"name": "john smith bsc", "given": "john", "family": "smith", "suffix": "bsc", "initials": "j. s.", "capitalized": "John Smith BSc"} {"name": "john smith ed", "given": "john", "family": "smith", "suffix": "ed", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "j. s.", "capitalized": "John Smith ED"} @@ -3834,7 +3853,7 @@ {"name": "van den Wolf Prof. Dr. Ada Cornelia Eva Sophia Jr, M.D. Ph.D.", "order": "FAMILY_FIRST", "given": "den Wolf Prof. Dr. Ada Cornelia Eva Sophia", "family": "van", "suffix": "Jr, M.D. Ph.D.", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "d. W. P. D. A. C. E. S. v.", "capitalized_force": "Den Wolf Prof. Dr. Ada Cornelia Eva Sophia Van Jr, M.D. Ph.D."} {"name": "van den Wolf, Prof. Dr. Anna Cornelia Eva Sophia jr. M.D. Ph.D.", "order": "FAMILY_FIRST", "title": "Prof. Dr.", "given": "Anna", "middle": "Cornelia Eva Sophia", "family": "van den Wolf", "suffix": "jr. M.D. Ph.D.", "initials": "A. C. E. S. W.", "capitalized_force": "Prof. Dr. Anna Cornelia Eva Sophia van den Wolf Jr. M.D. Ph.D."} {"name": "van den Wolf, prof. dr. Anna Cornelia Eva Sophia jr. m.d. ph.d.", "order": "FAMILY_FIRST", "title": "prof. dr.", "given": "Anna", "middle": "Cornelia Eva Sophia", "family": "van den Wolf", "suffix": "jr. m.d. ph.d.", "initials": "A. C. E. S. W.", "capitalized_force": "Prof. Dr. Anna Cornelia Eva Sophia van den Wolf Jr. M.D. Ph.D."} -{"name": "van der Berg, MA", "family": "van der Berg", "suffix": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "B."} +{"name": "van der Berg, MA", "family": "van der Berg", "suffix": "MA", "initials": "B."} {"name": "van der Berg, PhD", "family": "van der Berg", "suffix": "PhD", "initials": "B."} {"name": "van der Berg, Sir", "title": "Sir", "family": "van der Berg", "initials": "B."} {"name": "van ma van", "given": "van", "middle": "ma", "family": "van", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "v. m. v.", "capitalized": "Van Ma Van"} @@ -6203,7 +6222,7 @@ {"name": "김민준씨 (Jimmy)", "given": "민준", "family": "김", "suffix": "씨", "nickname": "Jimmy", "initials": "민. 김."} {"name": "김민준씨 Jr.", "given": "민준", "family": "김", "suffix": "씨 Jr.", "initials": "민. 김."} {"name": "김민준씨, J.씨", "family": "김민준", "suffix": "씨, J.씨", "initials": "김."} -{"name": "김민준씨, MA PhD", "family": "김민준", "suffix": "씨, MA PhD", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "김."} +{"name": "김민준씨, MA PhD", "family": "김민준", "suffix": "씨, MA PhD", "initials": "김."} {"name": "김민준씨, V.", "given": "V.", "family": "김민준", "suffix": "씨", "initials": "V. 김."} {"name": "김민준씨.", "given": "민준", "family": "김", "suffix": "씨.", "initials": "민. 김."} {"name": "김민준씨., J.씨", "family": "김민준", "suffix": "씨., J.씨", "initials": "김."} From 51ea0d54430cd9e279b637e4b836f524db204745 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sun, 25 Oct 2026 17:55:33 -0700 Subject: [PATCH 2/6] Settle a capitals member at the three sites the first version missed (#785) Found by the PR's code review (a grid of 104,676 parses against master, and each capitals member against its dotted spelling): - The comma's report on the part's opening word was withheld only where every listed member of the part was settled, so `Smith, MA PhD Ma` still reported its settled MA. It asks the word's own writing now. - P2's chain stopped at a suffix word but not at a settled member, so `Smith, Freiherr von Berg MA Jones` read given 'von Berg MA Jones' in silence where its dotted spelling reads suffix 'M.A. Jones'. `chain_run_end` asks `settled_by_case`, the chain's units with it. - P5's bound given-name join took a settled member as the given name's second word (`Smith, Abdul MA` -> given 'Abdul MA'); it declines one now, as it declines a suffix word. The design-docs review found the API reference's SUFFIX_OR_NAME docstring, rules/P6 and Standing lines in decisions/M2, S4 and S5 still stating the old reports, and S8's Accepted clause narrower than its cost (a family name written in capitals by convention, and a title word behind the member read as a title in silence); all amended, with rules/P2 and P5 and dated entries in decisions/S6, P2 and P5. The twin grid's capitals-against-dotted disagreements fall from 4,062 on master to 2,250, all of which master shares. ctgov, FEC, tse, PSC and the agreement records move exactly as before the fixes. Co-Authored-By: Claude Opus 5.5 --- .../design/decisions/M2-maiden-marker-rule.md | 4 +- .../decisions/P2-particles-join-forward.md | 1 + docs/design/decisions/P5-bound-given-names.md | 1 + ...-ambiguous-acronym-needs-words-to-spare.md | 2 +- ...arts-trailing-slot-after-a-family-comma.md | 4 +- .../S6-case-signal-at-the-suffix-slot.md | 9 +-- .../design/rules/P2-particles-join-forward.md | 3 +- docs/design/rules/P5-bound-given-names.md | 4 +- .../rules/P6-trailing-orphan-particle.md | 9 ++- ...arts-trailing-slot-after-a-family-comma.md | 2 +- .../S6-case-signal-at-the-suffix-slot.md | 4 +- .../rules/S8-credentials-trailing-run.md | 4 +- nameparser/_pipeline/_comma.py | 2 +- nameparser/_pipeline/_group.py | 15 +++-- nameparser/_pipeline/_pieces.py | 26 ++++++--- nameparser/_types.py | 54 ++++++++++-------- tests/v2/cases.py | 57 +++++++++++++++++-- tests/v2/rules_budget.json | 10 ++-- tests/v2/test_cases.py | 7 ++- tools/differential/corpus_rules.jsonl | 8 ++- tools/differential/corpus_shapes.jsonl | 1 + tools/differential/snapshot.jsonl | 8 ++- 22 files changed, 167 insertions(+), 68 deletions(-) diff --git a/docs/design/decisions/M2-maiden-marker-rule.md b/docs/design/decisions/M2-maiden-marker-rule.md index b1ba7947d..c460dbda4 100644 --- a/docs/design/decisions/M2-maiden-marker-rule.md +++ b/docs/design/decisions/M2-maiden-marker-rule.md @@ -13,11 +13,11 @@ - Where a trailing rule reads the part, a trailing title ends the clause, and a credential or numeral in front of it gets the stop it gets with the title absent: `Jane Doe nee Smith Prof.` → title 'Prof.', maiden 'Smith'; `Jane Doe nee Smith MA Prof.` and `… Prof. MA` both → maiden 'Smith', suffix 'MA'; `Jane Doe nee Smith V Prof.` → suffix 'V' — 2026-09-26 #535. - The title chain never takes the first word after the marker: `Jane Doe nee King.` keeps maiden 'King.', `Jane Doe nee Prof. Dr.` reads maiden 'Prof.', title 'Dr.', and a first word in the title vocabulary counts as a word, so `Jane Doe nee King. ba` gives up 'ba' as `Jane Doe nee Smith ba` does — 2026-09-26 #535. - Accepted, the transparency boundary: where the clause keeps the credential the two spellings differ, a title inside the kept text unable to leave without the words behind it (`Doe nee Smith ba Prof.` → title 'Prof.', maiden 'Smith ba'; `Doe nee Smith Prof. ba` → maiden 'Smith Prof. ba'), and H5's `King.` cost reaches a birth name (`Jane Doe nee Smith King.` → title 'King.', maiden 'Smith') — 2026-09-26 #535. -- Where a trailing rule reads the part, a member of the ambiguous class ending the maiden name is kept by the clause and reported, nobody writing a credential straight after the marker as its floor: `Jane Doe nee MA` → maiden 'MA', `Jane Doe nee Smith Ma` → maiden 'Smith Ma', and `Jane Doe nee Smith MA` → suffix 'MA', each reporting `suffix-or-name` — 2026-09-19 (Derek), #533. +- Where a trailing rule reads the part, a member of the ambiguous class ending the maiden name is kept by the clause and reported, nobody writing a credential straight after the marker as its floor: `Jane Doe nee MA` → maiden 'MA', `Jane Doe nee Smith Ma` → maiden 'Smith Ma', and `Jane Doe nee Smith MA` → suffix 'MA', each reporting `suffix-or-name` but the last, whose capitals settle it in silence since 2026-10-25 #785 (decisions/S6) — 2026-09-19 (Derek), #533. - Accepted with it: `JANE DOE NEE YO-YO MA` → maiden 'YO-YO', suffix 'MA', the one-case reading's cost; `Jane Doe nee Smith X.Y.Z.` → suffix 'X.Y.Z.', as `John Doe X.Y.Z.` reads; `Jane Smith nee Jones and MA` → maiden 'Jones and', suffix 'MA', the take running before the joins — 2026-09-19 (Derek), #533. - A word the clause gives up reads as a post-nominal or title or the clause keeps it, so where a trailing rule reads the part every word written after a taken marker is roled maiden, title or suffix (before a family comma it is not, as above); held since #601 by binding the clause-free reading as roles — 2026-09-19 (Derek), #533 review, A WORD; 2026-10-04 (Derek), #601. - Recorded rather than fixed: a bare delimiter token can end a clause, `Jane Doe nee Smith - MA` reading maiden 'Smith -', suffix 'MA' both by default and with `extra_suffix_delimiters=frozenset({"-"})`; #820 asks whether a lone dash should be a word at all — 2026-09-19 (Derek), #533 review, A WORD. -- Delimiters outrank every reading inside them: `Jane Doe (nee Smith MA)` and `Jane Doe (nee Smith Ma)` keep the whole span as the maiden name and report nothing, while `Jane Doe (nee Smith) MA` reads suffix 'MA' and reports — 2026-09-19 (Derek), #533 review, THE DELIMITERS TRUMP. +- Delimiters outrank every reading inside them: `Jane Doe (nee Smith MA)` and `Jane Doe (nee Smith Ma)` keep the whole span as the maiden name and report nothing, while `Jane Doe (nee Smith) MA` reads suffix 'MA', reported until its capitals settled it in 2026-10-25 #785 (`Jane Doe (nee Smith) ma` still reports) — 2026-09-19 (Derek), #533 review, THE DELIMITERS TRUMP. - Accepted, the exception: bracket content ending in a period is suffix-shaped (S1), so the brackets are dropped and the clause is read bare: `Jane Doe (nee Smith Prof.)` → title 'Prof.', maiden 'Smith' — 2026-09-26 #535. - A link joining two words of the clause does not end it, P3's class test asked with the clause's bounds: `Jane Doe nee Puig i Soler` → maiden 'Puig i Soler', `Maria Carod i Rovira nee Puig i Soler` → maiden 'Puig i Soler' with 'Carod i Rovira' kept whole, while `Jane Doe nee i Soler` declines (no maiden name), and `Jane Doe nee Puig i III`, `… Puig i MA` and `… Puig i` read suffix 'i III', 'i MA' and 'i' — 2026-09-20 (Derek), #397 review. - Recorded with it: the one-case spellings agree (`jane doe nee puig i soler` → maiden 'puig i soler'), `Jane Doe nee Puig i Ma` keeps maiden 'Puig i Ma' and reports, and `Jane Doe nee MA i Soler` reads maiden 'MA i Soler' in silence, as its `y` twin does — 2026-09-20 (Derek), #397 review. diff --git a/docs/design/decisions/P2-particles-join-forward.md b/docs/design/decisions/P2-particles-join-forward.md index 3f9c534cc..04a4d2c30 100644 --- a/docs/design/decisions/P2-particles-join-forward.md +++ b/docs/design/decisions/P2-particles-join-forward.md @@ -35,3 +35,4 @@ Open: none in this file. ROWS: five rows pinned the old decision and now read the new one (`fix(#624)`, each note giving the earlier reading): `Freiherr St van Berg MA`, `Freiherr Freiherr Prof do`, `St St VI`, `Freiherr St MA`, `Freiherr St Prof.`; `Freiherr St John Smith MA`, the issue's example, is added, and `Dr. Mc Mc` pins the give-back half (the review found that half unpinned, a revert passing the suite). The marker row that held group's particle-or-given emitter on `St St née` moves to `St van née`, the same structure with a chained particle that is not a title (parity), since `St St née` no longer reaches that emitter. - 2026-10-19 #760 — MOVED FROM the chain's code comment in `_group.py`, the facts the entries above did not hold. LINEAGE: the chain is v1's prefix chain — a non-leading prefix run absorbs everything to the next prefix or suffix — with v1's `leading_first_name` rule keeping the first piece a name (`Van Johnson`). #367's SHAPE, beside the `Sir de Mesnil` entry above: keyed on index 0, a title displaced the particle and the chain fired, so identical name text parsed two ways — `Van Johnson` given 'Van', family 'Johnson'; `Dr. Van Johnson` family 'Van Johnson'. SUFFIX PIECES ARE NOT SKIPPED when the chain looks for the leading piece, and the reason is what skipping them would do rather than what it would cost. The shapes that decide it are those whose leading piece lands in `given`: `Ph.D. Van Johnson`, `II Van Johnson` and `Msc.Ed. Van Johnson` each read given 'Ph.D.'/'II'/'Msc.Ed.' with family 'Van Johnson', and skipping the piece moves `Van` out of the family into the middle name (given 'Ph.D.', middle 'Van', family 'Johnson') — a worse reading on three shapes, fixing none. `Jr. Van Johnson`, the shape that looks as if it needs the skip, classifies its leading piece as a TITLE and is already covered by the title run. The example that opened this argument, `Ph. D. Van Johnson`, left it with #371 (the pair no longer merges at the head of the input, so there is no suffix-shaped leading piece to skip); `Dr. Ph. D. Van Johnson` replaces it — family 'Van Johnson' as shipped, family 'Johnson' with the skip. +- 2026-10-25 #785 — THE CHAIN STOPS AT A MEMBER ITS CAPITALS SETTLE, as at a suffix word, wherever it stands: the trailing read already ended the chain at one ending the name (decisions/S6's `Freiherr von Berg MA`), and a settled member with name words behind it, which the chain took into its run, now starts S8's credential run instead (`Smith, Freiherr von Berg MA Jones` → given 'von Berg', suffix 'MA Jones', as its dotted spelling reads). Found by #785's code review; the record is decisions/S6's review entry of this date. diff --git a/docs/design/decisions/P5-bound-given-names.md b/docs/design/decisions/P5-bound-given-names.md index cea6965b8..0ba6f92b5 100644 --- a/docs/design/decisions/P5-bound-given-names.md +++ b/docs/design/decisions/P5-bound-given-names.md @@ -60,3 +60,4 @@ Open: none in this file. DECIDED (Derek): this narrower clause (a′) rather than declining every title word (a). P5 states that a title word standing in the name is a name word and joins like one (H3), and (a) undid it: it failed six tests, a P5 rules example and the two tests named for that clause among them, and over the bound-given grid below moved 4,044 parses, 3,000 of them a join split with no title read (`(Bob), Abu Bishop` → given 'Abu', middle 'Bishop') beside the 1,044 that (a′) moves (six tests measured against 4455962f's suite). (a′) leaves every join on a title word inside the name standing -- `abdul Sir Smith Berg`, `Berg, abdul Prof. Ali`, `Smith, abdul Bishop`. MEASURED 2026-10-11 against 4455962f (#639's merge; this change is PR #643), over a 5,184-text bound-given grid (twelve heads × `abdul`, `Abu`, `abd al` × sixteen next words × nine tails) under the three orders: 1,044 parses move, every one a title word into the title field, 660 of them in a part past the second comma; #623's given-part grid moves 280, 260 of the same class and 20 the declined join below; no corpus name or case text but `Berg, abdul Prof.`, and nothing on the S2, #624 or #625 grids; the five gates exit 0; frames unmoved on the reference rows, up to `Berg, abdul Prof.` 207 → 228 on rare shapes no guard sees; recorded negative controls, 11 failing tests with no title declined and 5 with the past-the-second-comma half removed. THE REVIEW (2026-10-11). The decline after a family comma asks H5's run as the comma-less name reads it (`tail_reading`), and the given part's own walk reads some words behind a title otherwise, the disagreement #623's entry (decisions/H5) records for the chain's bound: there the join is declined and no title is read. A shape-only numeral VI to X, or V before a third comma part, which the walk keeps a name word (`Smith, abdul Prof. VI` → given 'abdul', middle 'Prof. VI', where master and 1.4.0 read given 'abdul Prof.', middle 'VI'); a period-marked title that is also a particle, which P6 then attaches with its report (`Smith, abdul St.` → family 'St. Smith', particle-or-given); and a bound word that is also a particle before one (`Smith, Abu St.` → title 'St.', where `Smith, Zed St.` reads family 'St. Smith'). Every such shape is garbage; the first two read as the same name with an ordinary given word does. Recorded, not narrowed, as #623's disagreement was (Derek, #623), and rules/P5 states it as an Accepted clause; the review's sweep found nothing reading worse than master on a real name and no comma-less name moving. It also found two mechanisms no test held, now rows: the bare-title half past the second comma (`John Smith, Jr., abdul Sir Smith`) and the whole trailing run after a family comma rather than the last word (`Berg, abdul Prof. Dr.`). +- 2026-10-25 #785 — THE JOIN DECLINES A MEMBER ITS CAPITALS SETTLE, as it declines a word of the unambiguous suffix vocabulary: in a name of mixed case a listed member of the ambiguous class written in capitals is the credential (rules/S6), so `Smith, Abdul MA` reads given 'Abdul', suffix 'MA', where 1.4.0 through 2.6.0 read given 'Abdul MA' (`Smith, abdul ma` still joins). Found by #785's code review; the record is decisions/S6's review entry of this date. diff --git a/docs/design/decisions/S4-ambiguous-acronym-needs-words-to-spare.md b/docs/design/decisions/S4-ambiguous-acronym-needs-words-to-spare.md index 8ba7a7b29..1df172d9d 100644 --- a/docs/design/decisions/S4-ambiguous-acronym-needs-words-to-spare.md +++ b/docs/design/decisions/S4-ambiguous-acronym-needs-words-to-spare.md @@ -3,7 +3,7 @@ ## Standing - At a comma the words-to-spare count reaches the listed ambiguous set as well as the shape class: two name words before the comma make the member the credential whatever its case (`John Smith, Ed`, `john smith, ma` → suffix), one leaves it the given name (`Royce, Ed`) — 2026-09-15. -- The comma path reports `suffix-or-name` whenever a member of the class is the first word after the comma, lean or none (`Smith, ma`, `毛泽东, MA`) — 2026-09-17. +- The comma path reports `suffix-or-name` whenever a member of the class is the first word after the comma, lean or none (`Smith, ma`, `毛泽东, MA`), save a listed member its capitals settle in a mixed-case name, which reports nothing (`Smith, MA`, `John Smith, MA`; 2026-10-25 #785, decisions/S6) — 2026-09-17. - Every decision at these slots reports the existing `suffix-or-name`; no new ambiguity kind was added — 2026-09-14. - Under strict mode, `Smith V., Ma` reads family 'V.', the vetoed word counting as a name word before the comma; left as it is and pinned, and recorded as the reach of the switch rules/C14 does not state — 2026-09-18. - A member ending a maiden marker's clause is read by the same count (`Jane Doe nee Smith MA` → maiden 'Smith', suffix 'MA'); a one-case name before the clause is the accepted exception, the clause hiding the member's case (`JANE DOE nee Smith Ma` → suffix 'Ma', where `JANE DOE Ma` keeps family 'Ma') — 2026-09-19 #533. diff --git a/docs/design/decisions/S5-given-parts-trailing-slot-after-a-family-comma.md b/docs/design/decisions/S5-given-parts-trailing-slot-after-a-family-comma.md index 2cf9e425d..75f2095d9 100644 --- a/docs/design/decisions/S5-given-parts-trailing-slot-after-a-family-comma.md +++ b/docs/design/decisions/S5-given-parts-trailing-slot-after-a-family-comma.md @@ -6,10 +6,10 @@ Examples name each field as the default order reads it; under the family-first o - After a family comma a member of the ambiguous class ending the given part is read by its writing, not the count, and reports whichever way it goes unless its capitals in a mixed-case name settled it, which reports nothing (2026-10-25 #785): `Doe, John MA` → suffix 'MA', `Doe, John Ma` → middle 'Ma', `Doe, John X.Y.Z.` → suffix 'X.Y.Z.'; the bare-acronym half restores 1.4.0's role, the dotted half leaves it to agree with `John Doe X.Y.Z.` — 2026-09-18 #531, THE COMMA REPORT'S REACH NOW. - Accepted with it: one-case and caseless names take the credential with no case evidence (`doe, john ma` → suffix 'ma', `田中, 太郎 MA` → suffix 'MA'), `Doe, John van MA` reads family 'van Doe', suffix 'MA', `Doe, John Prof. MA` gains title 'Prof.', and `DOE, JOHN ED` reads suffix 'ED' — 2026-09-18 #531, THE COMMA REPORT'S REACH NOW. -- `do`, the one member that is also a particle, is read by its capitals at this slot: in a mixed-case name `Doe, John DO` reads suffix 'DO', and every other spelling attaches to the family by P6, which reports the fork (`Doe, John do` → family 'do Doe'); the one-case `SMITH, JOHN DO` reading family 'DO SMITH' is the accepted cost, paired with `NASCIMENTO, EDSON ARANTES DO` — 2026-09-18 #531, CAPITALS DECIDE. +- `do`, the one member that is also a particle, is read by its capitals at this slot: in a mixed-case name `Doe, John DO` reads suffix 'DO', and every other spelling attaches to the family by P6, which reports the fork (`Doe, John do` → family 'do Doe'), unless a member its capitals settle stands in front and its run takes the word (`Doe, John MA do` → suffix 'MA do', 2026-10-25 #785); the one-case `SMITH, JOHN DO` reading family 'DO SMITH' is the accepted cost, paired with `NASCIMENTO, EDSON ARANTES DO` — 2026-09-18 #531, CAPITALS DECIDE. - A particle in front of the caps `DO` takes it out of the slot (`Doe, John van DO` → family 'van DO Doe'), and a particle behind a member that is not also an unambiguous suffix word ends the given part where this slot reads it (`Doe, John ma do` keeps middle 'ma'; a member its capitals settle starts the credential's run instead, which takes it, `Doe, John MA do` → suffix 'MA do', 2026-10-25 #785), while `vd` and `mc` are looked past (`Doe, John MA vd` → suffix 'MA', family 'vd Doe') — 2026-09-18 #531, CAPITALS DECIDE. - A never-given particle opening the given part leaves no word to spare, so the member behind it is the surname it heads unless mixed-case capitals say credential (`SMITH, VD MA` → family 'SMITH VD MA'; `Smith, de MA` → suffix 'MA') — 2026-10-02 #573. -- Two report movements stay as recorded: `van MA` reports only `suffix-or-name`, and `John Smith Ma Ma` reports once; and `John Smith www.example.com` reads suffix 'www.example.com', reported, the dotted shape unable to tell a URL from an acronym — 2026-09-18. +- Two report movements stay as recorded: `van MA` reports only `suffix-or-name` (none since 2026-10-25 #785, its capitals settling it: `John Smith van MA` is silent and `Doe, John van MA` reports P6's `particle-or-given` alone), and `John Smith Ma Ma` reports once; and `John Smith www.example.com` reads suffix 'www.example.com', reported, the dotted shape unable to tell a URL from an acronym — 2026-09-18. Not standing: the split of 2026-10-17 #682, carried out. diff --git a/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md b/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md index 2edb844a7..98822787b 100644 --- a/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md +++ b/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md @@ -10,7 +10,7 @@ - `Freiherr von Berg MA` reads family 'von Berg', suffix 'MA', the lean ending P2's chain with no words to spare — 2026-09-15 (Derek). - The whole-PR review's five readings stand: an acronym the particle chain absorbs reports `suffix-or-name` (`John van der Berg Ma`); the CJK stage asks the post-comma run with the one-case fact; a by-shape credential in a tail segment raises no `comma-structure`; a word a caller lists as a surname is never caps-shaped; and a dotted listed member takes the lean — 2026-09-18, WHOLE-PR REVIEW ROUND. -- A listed member of `suffix_acronyms_ambiguous` written in capitals in a name of mixed case is settled as the credential by its writing and reports nothing at any slot that reads it (`John Smith MA`, `Jack MA` but its `given-or-family`, `John Smith, MA`, `Doe, John DO`), and it starts the credential's trailing run (rules/S8: `John Smith MA Jones` → suffix 'MA Jones'); the clash settled is the one with the name reading alone, so the word keeps its tags and its other readings (`Doe, John DO` keeps family 'Doe'), a maiden clause still keeps it as its first word and reports it (`Jane Doe nee MA`), and nothing before a comma leaves it the given name, reported (`, MA`) — 2026-10-25 #785. +- A listed member of `suffix_acronyms_ambiguous` written in capitals in a name of mixed case is settled as the credential by its writing and reports nothing at any slot that reads it (`John Smith MA`, `Jack MA` but its `given-or-family`, `John Smith, MA`, `Doe, John DO`), and it starts the credential's trailing run (rules/S8: `John Smith MA Jones` → suffix 'MA Jones'); the clash settled is the one with the name reading alone, so the word keeps its tags and its other readings (`Doe, John DO` keeps family 'Doe'), a maiden clause still keeps it as its first word and reports it (`Jane Doe nee MA`), and nothing before a comma leaves it the given name, reported (`, MA Smith`) — 2026-10-25 #785 (Derek, 2026-10-23); 2026-10-25 #785, review. Elsewhere: the case a generation word that is also a name reads (rules/S6's bullet on it) is decisions/S11's 2026-10-25 #752 entry. @@ -36,8 +36,9 @@ Open: none in this file. - 2026-10-20 (Derek), #761 — "A COMMA", NOT "A FAMILY COMMA". rules/S6 said the count decides first at the first slot after a family comma, where rules/S4 said after a comma; the parser follows S4 at a suffix comma too (`John Smith, Ma` reads suffix `Ma`, reported). Found in the skeleton migration (PR #783); S6's Precedence lead now says "a comma". No behavior moves. - 2026-10-25 #785 (Derek, 2026-10-23) — CAPITALS SETTLE A LISTED CREDENTIAL, AND A CLASH SETTLED BY THE WRITING IS NOT REPORTED (#752's phase 2g: the form step of decisions/vocabulary-model's 2026-10-23 #752 entry, item 7, answering #785's first three questions; its fourth was answered there, no new kind). In a name written in more than one case a listed member of the ambiguous class written in capitals was already READ as the credential (the 2026-09-14 entries above); it was still REPORTED as a fork at every slot, while the same word written with its periods (`M.A.`, rules/S3) reported nothing, and it did not start S8's run where the dotted spelling did. #785 asked whether the two writings should answer alike: they do now. `John Smith MA` reports nothing where it reported `suffix-or-name`; `John Smith MA Jones` reads suffix 'MA Jones', reported for 'Jones', where it read middle 'Smith MA', family 'Jones'; a name written in one case keeps its report (`JOHN SMITH MA`), and so does a Title-case member, which leans to the name and is weighed by the count. HOW, AND WHY NOT A RETAG. The phase 2 spike settled the word by retagging it as an unambiguous credential, and that erased its OTHER readings: `do` is a particle too, and once it stopped being a member of the ambiguous class rules/P6's attachment took it (`Doe, John DO` → family 'DO Doe'), and the maiden clause, whose first word is kept by the clause whatever it is, lost its maiden name (`Jane Doe nee MA`). So the word keeps every tag, and one predicate (`_pieces.settled_by_case`, a listed acronym whose lean is the credential, a suffix word that is also a name being rules/S11's) is asked where a reading is chosen: the trailing peel takes a settled member without recording a pick, so no reporter downstream meets it; the comma's decision and the given part's trailing slot leave its report out; and S8's run start admits it, the particle test in front still refusing `DO` (`John Smith DO Jones` keeps family 'DO Jones'). The case asked is ParseState's one-case fact over the name's own words, so a word in a maiden clause or in brackets is read by the whole name's case, never its own; the spike asked a bracketed word's own case and moved 7 FEC reports for nothing. - WHERE IT STILL REPORTS. Where something before the writing keeps the word a name it is read as before and reported: the first word a maiden clause takes (rules/M2, `Jane Doe nee MA` → maiden 'MA'), and the word after a comma with nothing in front of it, which C4's count leaves the given name (`, MA` → given 'MA'). A bracketed member is rules/S1's, unchanged here (phase 2l). + WHERE IT STILL REPORTS. Where something before the writing keeps the word a name it is read as before and reported: the first word a maiden clause takes (rules/M2, `Jane Doe nee MA` → maiden 'MA'), and the word after a comma with nothing in front of it, which C4's count leaves the given name (`, MA Smith` → given 'MA'). A bracketed member is rules/S1's, unchanged here (phase 2l). ACCEPTED. A writer's initials in capitals between the given names and the family name, where they spell a listed member, start the run, and the family name reads as a suffix (rules/S8's Accepted clause, `Maria Ana MA Silva` → suffix 'MA Silva', as `Maria Ana M.A. Silva` already read). In ctgov-officials (fetched 2026-10-21), 9 distinct names write a listed member in capitals between two Title-case name words with no comma, and 6 of them now read this way. The other 5 field moves there are lists of credentials after a comma that the parse reads as a reversed name, a credential taken for the given name (unchanged), whose list behind the member now reads as the suffix where it read as middle names. No name in that shape is written with the periods there. - MEASURED (2026-10-25, Python 3.11.17, against master a46b3a6e, every local check with `--base` that checkout): the committed snapshot moves 54 names, 48 a dropped report and 6 a field, every one a case-row or rules text, and gains 20 texts and loses 1, the rows added and those rewritten to keep reaching their emitter; every CLDR, GND, Wikidata and locale agreement record matches. ctgov-officials moves 1,778 readings: 1,767 drop a report and nothing else, each a capitals listed member (`DO` 1,090, `MA` 426, `BA` 184, `JD` 35, `PA` 11, the rest five or fewer; by shape `Aa Aa, DO` 607, `Aa Aa Aa, DO` 244, `Aa Aa, MA` 176), and 11 move a field, each the run a settled member starts (the 6 accepted above and the 5 credential lists); none gains a report. fec-individuals, written in one case, and tse-donors match the base; psc-individuals matches it on every record (1,004,785 of 1,039,171 agreeing). The phase 2 spike's figures (71 committed, 1,853 ctgov, 7 FEC) were taken on another base with the retag, and are not this change's. + MEASURED (2026-10-25, Python 3.11.17, against master a46b3a6e, every local check with `--base` that checkout): the committed snapshot moves 54 names, 48 a dropped report and 6 a field, every one a case-row or rules text, and gains 20 texts and loses 1, the rows added and those rewritten to keep reaching their emitter; every CLDR, GND, Wikidata and locale agreement record matches. ctgov-officials moves 1,778 readings: 1,767 drop a report and nothing else, each a capitals listed member (`DO` 1,090, `MA` 426, `BA` 184, `JD` 35, `PA` 11, the rest five or fewer; by shape `Aa Aa, DO` 607, `Aa Aa Aa, DO` 244, `Aa Aa, MA` 176), and 11 move a field, each the run a settled member starts (the 6 accepted above and the 5 credential lists), 10 of them gaining the report on the name word the run absorbs; no other reading gains a report. fec-individuals, written in one case, and tse-donors match the base; psc-individuals matches it on every record (1,004,785 of 1,039,171 agreeing). The phase 2 spike's figures (71 committed, 1,853 ctgov, 7 FEC) were taken on another base with the retag, and are not this change's. FRAMES (`sys.setprofile` call events over a second parse, py3.11): the reference row and plain names cost what they did (`Dr. Juan0001 de la Vega III` 305, `John Smith` 149, `Smith, John` 158, `John Smith, PhD` 202); a name holding a member pays for the predicate where it is asked (`John Smith Ma` 213 → 220, `JOHN SMITH MA` 212 → 215, `John Smith MA` 212 → 203, the pick and its report gone) and for the run it now reads (`John Smith MA Jones` 232 → 259). - Recompute: `uv run python tools/differential/snapshot.py --since `; for each local corpus `snapshot.py --local --base `, and `local_agreement.py psc-individuals --base `; the ctgov shape count by reading `corpus.jsonl` of ctgov-officials, keeping each distinct name whose part before its first comma holds, after two words, a word whose letters are a listed member written in capitals between a Title-case word and the next, and counting those whose suffix holds it, on this tree and on the parent. + Recompute: `uv run python tools/differential/snapshot.py --since `; for each local corpus `snapshot.py --local --base `, and `local_agreement.py psc-individuals --base `; the ctgov shape count by reading `corpus.jsonl` of ctgov-officials, keeping each distinct name whose part before its first comma holds, after two words, a word whose letters are a listed member written in capitals between two words that each open with a capital and are not wholly in capitals, and counting those whose suffix holds it, on this tree and on the parent. +- 2026-10-25 #785, review — THREE SITES THE FIRST VERSION LEFT ASKING THE OLD QUESTION, found by the PR's code review on a grid of 104,676 parses (125 shapes, 23 spellings of the member as written, in upper and in lower case, three orders, four policies) against master, and a twin grid comparing each capitals member with its dotted spelling. (i) The comma's report on the part's opening word was withheld only where every listed member of the part was settled, so `Smith, MA PhD Ma` still reported its settled `MA` beside the Title-case `Ma` S7's company reads; it asks the word's own writing now. (ii) P2's chain stopped at a suffix word and not at a settled member, so after a family comma with a title-particle head the chain took the member and the words behind it into the given name in silence (`Smith, Freiherr von Berg MA Jones` → given 'von Berg MA Jones', where `Smith, Freiherr von Berg M.A. Jones` reads suffix 'M.A. Jones'); `_pieces.chain_run_end` asks `settled_by_case` now, the chain's units with it, and the member starts the run. (iii) P5's bound given-name join took a settled member as the given name's second word (`Smith, Abdul MA` → given 'Abdul MA' in silence, where `Smith, Abdul M.A.` reads suffix 'M.A.'); it declines one now, as it declines a suffix word (decisions/P5). Measured after the fixes, against the same master: the twin grid's capitals-against-dotted disagreements fall from 4,062 on master to 2,250, every one of which master shares; the main grid's removed reports all name a settled member and no report is added; ctgov-officials, fec-individuals and tse-donors move exactly as the entry above records, the three sites reaching no name in them. The design-docs review found the API reference (`AmbiguityKind.SUFFIX_OR_NAME`'s docstring), rules/P6 and Standing lines in decisions/M2, S4 and S5 still stating the old reports, and S8's Accepted clause narrower than its cost: a family name written in capitals by convention reads the same way (`Jean Pierre MA DUPONT` → suffix 'MA DUPONT'), and a name word behind the member that is title vocabulary reads as a title in silence (`Mary Jane MA King` → title 'King', as `Mary Jane M.A. King` reads); all are amended. Left as it reads: `John Smith MA y` reads family 'MA y' where `John Smith PhD y` reads suffix 'PhD y', a trailing connective no writing produces. diff --git a/docs/design/rules/P2-particles-join-forward.md b/docs/design/rules/P2-particles-join-forward.md index c788f0903..daa7c3fb9 100644 --- a/docs/design/rules/P2-particles-join-forward.md +++ b/docs/design/rules/P2-particles-join-forward.md @@ -8,8 +8,9 @@ ``` "Dr. John van der Berg" → family="van der Berg" ``` -- **Where a trailing suffix begins** is the trailing run S2 reads, once, before the chain is made and counting the chain's own run as the one name word it will be, so the chain cannot change what it counts: a trailing roman numeral S10 reads as the generation, or a bare acronym with words to spare, ends the chain as a suffix word does, and so does a bare ambiguous acronym written in capitals in a mixed-case name (S6), which needs no words to spare. +- **Where a trailing suffix begins** is the trailing run S2 reads, once, before the chain is made and counting the chain's own run as the one name word it will be, so the chain cannot change what it counts: a trailing roman numeral S10 reads as the generation, or a bare acronym with words to spare, ends the chain as a suffix word does, and so does a bare ambiguous acronym written in capitals in a mixed-case name (S6), which needs no words to spare, wherever it stands in the name: settled by its writing, it is a suffix word to the chain, and the run it starts is S8's. ``` + "Smith, Freiherr von Berg MA Jones" → given="von Berg" "John van der Berg PhD" → family="van der Berg" "John van der Berg V" → family="van der Berg" "John van der Berg V" → suffix="V" diff --git a/docs/design/rules/P5-bound-given-names.md b/docs/design/rules/P5-bound-given-names.md index bf6cd98d1..a25d293ef 100644 --- a/docs/design/rules/P5-bound-given-names.md +++ b/docs/design/rules/P5-bound-given-names.md @@ -51,8 +51,10 @@ **Except:** -- **The join never absorbs a marker standing as a word of its own** — a marker is not a name word (M2) — nor a word of the unambiguous suffix vocabulary (S2), wherever position will then place it, nor a credential split across a space (S12) that the given part after a family comma holds, nor a title word a trailing rule reads as a title: one the trailing title run (H5) takes from the given part after a family comma, read as the comma-less name reads it with the bare salutations H8 adds, or any title word in a part past the second comma (C2). +- **The join never absorbs a marker standing as a word of its own** — a marker is not a name word (M2) — nor a word of the unambiguous suffix vocabulary (S2), wherever position will then place it, nor a member of the ambiguous class its capitals settle as the credential in a name of mixed case (S6), nor a credential split across a space (S12) that the given part after a family comma holds, nor a title word a trailing rule reads as a title: one the trailing title run (H5) takes from the given part after a family comma, read as the comma-less name reads it with the bare salutations H8 adds, or any title word in a part past the second comma (C2). ``` + "Berg, abdul MA" → suffix="MA" + "Berg, abdul ma" → given="abdul ma" "abdul Jr Smith Berg" → given="abdul" "abdul Jr Smith Berg" → middle="Jr Smith" "abdul Ph. D. Smith Berg" → suffix="Ph. D." diff --git a/docs/design/rules/P6-trailing-orphan-particle.md b/docs/design/rules/P6-trailing-orphan-particle.md index 0b95dee49..449ca7e54 100644 --- a/docs/design/rules/P6-trailing-orphan-particle.md +++ b/docs/design/rules/P6-trailing-orphan-particle.md @@ -27,7 +27,7 @@ ``` - **Nothing is asked about the WORD**, and that is what lets one rule read both traditions: the same vocabulary that spells the Dutch tussenvoegsel spells the Vietnamese given name, and the declared order separates them where a comma cannot — one order, not both, as an Accepted bullet below records. - **Where nothing has named the family, the particle is read where it stands**, and a particle CAN be a given name there: the never-given vocabulary supplies the reading position leaves open, and forbids nothing (parse("de") reports given "de", and so does every other word in that set). -- **What reports.** After a family comma, a particle that is also of the AMBIGUOUS credential class (`do`), where neither S6's capitals nor S7's company below reaches it, attaches, and the report gives it this rule's particle fork rather than S4's credential one. The fork is reported, in the kind naming the reading this attachment OVERRODE — what the run was read as before it fired, not what its words are. Where the run was read as a post-nominal, which is the S2 reading this rule outranks, the report is suffix-or-name. Otherwise, where the run holds an ambiguous particle, the reading overridden is that word as a name word, and the report is particle-or-given. Where the attachment overrode neither — a never-given particle read as a name word, which it could never have been — nothing was decided and nothing is reported. So after a family comma `do`, which sits in the AMBIGUOUS acronym half and was already read as a name word, reports particle-or-given while vd and mc report suffix-or-name, and a run of several words read as name words reports on its ambiguous member if it has one and otherwise reports nothing, whatever its words could have been read as standing alone. Both are pinned in tests/v2/cases.py. +- **What reports.** After a family comma, a particle that is also of the AMBIGUOUS credential class (`do`), where neither S6's capitals nor S7's company nor a run a settled member starts reaches it (below), attaches, and the report gives it this rule's particle fork rather than S4's credential one. The fork is reported, in the kind naming the reading this attachment OVERRODE — what the run was read as before it fired, not what its words are. Where the run was read as a post-nominal, which is the S2 reading this rule outranks, the report is suffix-or-name. Otherwise, where the run holds an ambiguous particle, the reading overridden is that word as a name word, and the report is particle-or-given. Where the attachment overrode neither — a never-given particle read as a name word, which it could never have been — nothing was decided and nothing is reported. So after a family comma `do`, which sits in the AMBIGUOUS acronym half and was already read as a name word, reports particle-or-given while vd and mc report suffix-or-name, and a run of several words read as name words reports on its ambiguous member if it has one and otherwise reports nothing, whatever its words could have been read as standing alone. Both are pinned in tests/v2/cases.py. ``` "Nguyen, Thi Van" → ambiguities=("particle-or-given",) "Berg, Jan vd" → ambiguities=("suffix-or-name",) @@ -72,7 +72,12 @@ "Doe, John Do" → family="Do Doe" "Doe, John van DO" → family="van DO Doe" ``` -- **S7's company** (an ambiguous credential behind an unambiguous one): such a word standing behind an unambiguous credential in one run of suffix words reads as the credential in any spelling and any case, reported as S4's credential fork. +- **S7's company** (an ambiguous credential behind an unambiguous one): such a word standing behind an unambiguous credential in one run of suffix words reads as the credential in any spelling and any case, reported as S4's credential fork unless its own capitals settled it (S6). +- **A run a settled member starts** ("Doe, John MA do"): a member of the ambiguous class its capitals settle starts the credential's run (S8), and a lone member of the class behind it is the run's, as behind an unambiguous credential, so this attachment stands down. + ``` + "Doe, John MA do" → family="Doe" + "Doe, John ma do" → family="do Doe" + ``` - **P9's run** (a lone word of both vocabularies inside a run of post-nominals): a lone word of both the particle and the UNAMBIGUOUS suffix vocabulary standing inside a run read as post-nominals keeps that reading, and this attachment stands down. **Accepted:** diff --git a/docs/design/rules/S5-given-parts-trailing-slot-after-a-family-comma.md b/docs/design/rules/S5-given-parts-trailing-slot-after-a-family-comma.md index 52705aaae..78c7abe3a 100644 --- a/docs/design/rules/S5-given-parts-trailing-slot-after-a-family-comma.md +++ b/docs/design/rules/S5-given-parts-trailing-slot-after-a-family-comma.md @@ -49,7 +49,7 @@ **Precedence:** -- **The member of the class that is particle vocabulary as well, standing alone at this slot** ("Doe, John Do"): P6 decides it: the capitals take it as the credential, as does an unambiguous credential in front of it (S7), and every other spelling attaches to the family, reported there as P6's fork rather than as this one. Behind another particle it does not stand alone — the two are one particle run by then — and the run attaches whatever the capitals say (P6). +- **The member of the class that is particle vocabulary as well, standing alone at this slot** ("Doe, John Do"): P6 decides it: the capitals take it as the credential, as does an unambiguous credential in front of it (S7) or a member its capitals settle, which starts the credential's run (S8), and every other spelling attaches to the family, reported there as P6's fork rather than as this one. Behind another particle it does not stand alone — the two are one particle run by then — and the run attaches whatever the capitals say (P6). ``` "SMITH, JOHN DO" → family="DO SMITH" · boundary ``` diff --git a/docs/design/rules/S6-case-signal-at-the-suffix-slot.md b/docs/design/rules/S6-case-signal-at-the-suffix-slot.md index b16996bb0..0d72d5930 100644 --- a/docs/design/rules/S6-case-signal-at-the-suffix-slot.md +++ b/docs/design/rules/S6-case-signal-at-the-suffix-slot.md @@ -42,8 +42,8 @@ - **Where something before the writing keeps the word a name, it reports as before**: with nothing in front of a comma C4's count leaves the word after it the given name, the case being read only behind one name word (C4), and the first word after a maiden marker is the maiden name the marker announced (M2), whatever case either is written in. ``` - ", MA" → given="MA" · boundary - ", MA" → ambiguities=("suffix-or-name",) · boundary + ", MA Smith" → given="MA" · boundary + ", MA Smith" → ambiguities=("suffix-or-name",) · boundary "Jane Doe nee MA" → ambiguities=("suffix-or-name",) · boundary ``` diff --git a/docs/design/rules/S8-credentials-trailing-run.md b/docs/design/rules/S8-credentials-trailing-run.md index 9f4ab8b7b..652383014 100644 --- a/docs/design/rules/S8-credentials-trailing-run.md +++ b/docs/design/rules/S8-credentials-trailing-run.md @@ -64,10 +64,12 @@ **Accepted:** -- A writer's own initials, written in capitals between the given names and the family name, that spell a listed member of the ambiguous class start the run as that credential does, and the family name behind them reads as a suffix, reported: the capitals are the credential's writing and the initials' alike, and the periods' spelling already read this way. +- A word of capitals between two name words that spells a listed member of the ambiguous class starts the run as that credential does, whether it is the writer's own initials or a family name written in capitals by the convention that writes it so, and the name word behind it reads as a suffix, reported, or as a title where it is title vocabulary, in silence: the capitals are the credential's writing and these alike, and the periods' spelling already read this way. ``` "Maria Ana MA Silva" → suffix="MA Silva" "Maria Ana M.A. Silva" → suffix="M.A. Silva" + "Jean Pierre MA DUPONT" → suffix="MA DUPONT" + "Mary Jane MA King" → title="King" ``` --- diff --git a/nameparser/_pipeline/_comma.py b/nameparser/_pipeline/_comma.py index 19af223d9..9e50cd11d 100644 --- a/nameparser/_pipeline/_comma.py +++ b/nameparser/_pipeline/_comma.py @@ -651,7 +651,7 @@ def decide(state: ParseState, splits: bool | None = True) -> ParseState: f"{what}; the part before the comma holds {names} name words, " f"so it is read as a credential run", tuple(part))) elif (len(p1[0]) == 1 and p1[0][0] in listed - and not (settled and reading[0] + and not (reading[0] and settled_by_case(tokens[p1[0][0]], state.one_case))): # the first word after the comma reports as it always does, # naming the role bound above: the listing form binds a word diff --git a/nameparser/_pipeline/_group.py b/nameparser/_pipeline/_group.py index 6c72c705c..32750c813 100644 --- a/nameparser/_pipeline/_group.py +++ b/nameparser/_pipeline/_group.py @@ -71,13 +71,14 @@ Peel, TailRead, chain_lead, chain_run_end, is_conj_piece, is_leading_title, is_prefix_piece, is_suffix_piece, is_title_piece, join_connectives, joined_tags, leading_titles, merge_particle_runs, merge_pieces, peel_walk, - read_trailing_run, tail_reading, trailing_candidates, + read_trailing_run, settled_by_case, tail_reading, trailing_candidates, trailing_run_start, trailing_start, trailing_start_past_titles, ) from nameparser._pipeline._state import ( - SPLIT_CREDENTIAL_TAG, SPLIT_MEMBER_TAG, ParseState, PendingAmbiguity, - Structure, WorkToken, _AMBIGUOUS_CREDENTIAL_TAGS, copy_with, + AMBIGUOUS_ACRONYM_TAG, SPLIT_CREDENTIAL_TAG, SPLIT_MEMBER_TAG, ParseState, + PendingAmbiguity, Structure, WorkToken, _AMBIGUOUS_CREDENTIAL_TAGS, + copy_with, ) import nameparser._pipeline._comma as _comma from nameparser._pipeline._vocab import ( @@ -960,7 +961,7 @@ def chain(tail: int) -> None: k += 1 continue j = chain_run_end(k, pieces, ptags, tokens, - len(pieces) - tail) + len(pieces) - tail, one_case) # The other half of PARTICLE_OR_GIVEN. _assign reports the # fork when an ambiguous particle stays a lone leading piece # ("Van Johnson" -> given under the default order, family @@ -1162,10 +1163,16 @@ def chain(tail: int) -> None: # as its one-word spelling is: merged in the given part # before this join, so the particle chain stops in front of # it too ('Rahman, Abdul MB ChB', 'Smith, John van Pharm D') + # A member its capitals settle as the credential (rules/S6) + # is declined as a suffix word is: the join would read it as + # the given name's second word ('Smith, Abdul MA') nxt, nxt_tags = pieces[fk + 1], ptags[fk + 1] if (_is_maiden_marker_piece(nxt, tokens) or is_suffix_piece(nxt, nxt_tags, tokens) or "split" in nxt_tags + or (len(nxt) == 1 + and AMBIGUOUS_ACRONYM_TAG in tokens[nxt[0]].tags + and settled_by_case(tokens[nxt[0]], one_case)) or ((site is ClauseSite.TAIL or bound_join is BoundJoin.LENIENT) and is_title_piece(nxt, nxt_tags, tokens) diff --git a/nameparser/_pipeline/_pieces.py b/nameparser/_pipeline/_pieces.py index f0af83c7f..4ec5745f3 100644 --- a/nameparser/_pipeline/_pieces.py +++ b/nameparser/_pipeline/_pieces.py @@ -1998,15 +1998,19 @@ class TailRead(NamedTuple): # units the read counts are the ones the chain then builds def chain_run_end(k: int, pieces: Sequence[Sequence[int]], ptags: Sequence[Set[str]], - tokens: Sequence[WorkToken], end: int) -> int: + tokens: Sequence[WorkToken], end: int, + one_case: bool | None = None) -> int: """Where the chain run opened by the particle `pieces[k]` ends: past the particles straight behind it, then past the name words up to - `end`, a suffix piece, or the next particle. `pieces[k:j]` is the - one name unit P2 makes of them; `j == k + 1` where it makes none. + `end`, a suffix piece, a member its capitals settle as the + credential (`settled_by_case`, with `one_case` ParseState's), or + the next particle. `pieces[k:j]` is the one name unit P2 makes of + them; `j == k + 1` where it makes none. The particle test is `is_prefix_piece`'s definition written inline: the walk runs per piece of every chained name (AGENTS.md's frame - budget, question 1).""" + budget, question 1), and so is the member tag ahead of the + settled test.""" count = len(pieces) j = k + 1 while j < count and ("prefix" in ptags[j] @@ -2016,7 +2020,12 @@ def chain_run_end(k: int, pieces: Sequence[Sequence[int]], while (j < end and "prefix" not in ptags[j] and not (len(pieces[j]) == 1 and "particle" in tokens[pieces[j][0]].tags) - and not is_suffix_piece(pieces[j], ptags[j], tokens)): + and not is_suffix_piece(pieces[j], ptags[j], tokens) + # rules/S6: "a clash settled by the writing is not reported" + # -- and the chain does not take the settled credential in + and not (len(pieces[j]) == 1 + and AMBIGUOUS_ACRONYM_TAG in tokens[pieces[j][0]].tags + and settled_by_case(tokens[pieces[j][0]], one_case))): j += 1 return j @@ -2076,7 +2085,8 @@ def merge_particle_runs(pieces: list[list[int]], ptags: list[set[str]], def _chain_units(pieces: Sequence[Sequence[int]], ptags: Sequence[Set[str]], tokens: Sequence[WorkToken], - n: int, lead: int | None = None) -> list[int] | None: + n: int, lead: int | None = None, + one_case: bool | None = None) -> list[int] | None: """The name units P2's chain will make of `pieces`, as a mark per piece -- OPENS where a piece opens a unit, and JOINED where the chain will join a name word to the run in front of it and the @@ -2109,7 +2119,7 @@ def _chain_units(pieces: Sequence[Sequence[int]], else lead) + 1 while k < count: if prefix[k]: - j = chain_run_end(k, pieces, ptags, tokens, count) + j = chain_run_end(k, pieces, ptags, tokens, count, one_case) q = k + 1 # a split credential is weighed as the class it is a # member of (rules/S12) @@ -2177,7 +2187,7 @@ def read_trailing_run(pieces: Sequence[Sequence[int]], mc mc' in the family.""" if n == len(pieces): return None - units = _chain_units(pieces, ptags, tokens, n, lead) + units = _chain_units(pieces, ptags, tokens, n, lead, one_case) rest, titled, peel = tail_reading(peel_walk(n, ptags), pieces, ptags, tokens, one_case, units) tail: set[int] = set() diff --git a/nameparser/_types.py b/nameparser/_types.py index efe70fdfa..fc860d12f 100644 --- a/nameparser/_types.py +++ b/nameparser/_types.py @@ -448,7 +448,7 @@ class AmbiguityKind(StrEnum): SUFFIX_OR_NICKNAME = "suffix-or-nickname" #: A trailing word reads plausibly as either a post-nominal or an #: ordinary name part. Covers an ambiguous acronym written without - #: periods ("John Smith MA" takes MA as a credential because a + #: periods ("JOHN SMITH MA" takes MA as a credential because a #: family name remains; "JACK MA", written wholly in one case, keeps #: it as the name because none would, while in a mixed-case name #: the capitals decide outright -- "Jack MA" is the credential and @@ -461,6 +461,11 @@ class AmbiguityKind(StrEnum): #: vd" takes ``vd`` for *van der* and declines the decoration. #: Which name part was declined depends on position and #: ``name_order``, so ``detail`` names it rather than the kind. + #: Since 2.7 a listed member its capitals settle in a mixed-case + #: name is not reported at any slot below, as one written with its + #: periods is not ("John Smith MA", "Doe, John DO", "John Smith, + #: MA" are silent, rules/S6, #785), so the examples below that + #: report are written in one case, in lower case or in Title case. #: The same doubt covers an input that is nothing BUT post-nominal #: vocabulary ("Rinpoche", "QC MP"): with no name word beside it #: the first post-nominal is read as the name, because something @@ -474,7 +479,7 @@ class AmbiguityKind(StrEnum): #: comma is not among them, so "Smith, John, MA" is silent; what #: such a part does report is rules/C2's to say. #: Since 2.4 the given part's trailing slot reports whichever way - #: it read the word: "Doe, John MA" reads suffix ``MA`` and says + #: it read the word: "DOE, JOHN MA" reads suffix ``MA`` and says #: so, "Doe, John Ma" keeps middle ``Ma`` and says so too. Not in #: EVERY direction, though: where the member is also a particle #: and the particle rule keeps it, that rule reports at its own @@ -490,11 +495,11 @@ class AmbiguityKind(StrEnum): #: that the company leaves holding no name word ("John Smith PhD #: MEng", "Doe, Jane PhD MEng" and "Smith, PhD MEng" each report #: ``MEng``). A member its own capitals already made the - #: credential reports nothing new, so "Smith, PhD MA" and "John + #: credential reports nothing, so "Smith, PhD MA" and "John #: Smith, PhD MA" read suffix ``PhD MA`` in silence. #: Since 2.4 a maiden marker's clause reports at ITS trailing slot - #: too, in both directions: "Jane Doe nee Smith MA" gives maiden - #: ``Smith`` with suffix ``MA`` and says so, "Jane Doe nee Smith + #: too, in both directions: "Jane Doe nee Smith ma" gives maiden + #: ``Smith`` with suffix ``ma`` and says so, "Jane Doe nee Smith #: Ma" keeps maiden ``Smith Ma`` and says so too, and a member the #: clause keeps because it is the only word after the marker #: ("Jane Doe nee MA") reports as well. Where no trailing rule @@ -508,13 +513,15 @@ class AmbiguityKind(StrEnum): #: them, and the three that follow are about the member's own #: surroundings. Second: a member with something BEHIND it that #: the trailing reading does not take was never a fork -- "Doe, - #: John MA Smith" reads middle ``MA Smith``, the ordinary reading, + #: John ma Smith" reads middle ``ma Smith``, the ordinary reading, #: and nothing consulted the class, and inside a clause the same #: holds of a name word ("Jane Doe nee MA Smith" keeps maiden #: ``MA Smith``), a bare trailing title among them ("Jane Doe nee - #: Smith MA Prof" keeps maiden ``Smith MA Prof``, where the - #: period-marked "Jane Doe nee Smith MA Prof." reads title - #: ``Prof.`` and suffix ``MA`` and reports). + #: Smith ma Prof" keeps maiden ``Smith ma Prof``, where the + #: period-marked "Jane Doe nee Smith ma Prof." reads title + #: ``Prof.`` and suffix ``ma`` and reports). A member its capitals + #: settle is no such member: it starts the credential's run + #: (rules/S8), so "Doe, John MA Smith" reads suffix ``MA Smith``. #: Third -- pre-existing, and untouched by 2.4 -- a member with no #: name word IN FRONT of it is not at this #: slot either, because the slot is the end of a given part and @@ -531,8 +538,8 @@ class AmbiguityKind(StrEnum): #: the same member, one title in front of it -- reads given ``MA`` #: in silence. A maiden marker cannot reach this position: after #: a family comma it is an ordinary word (rules/M2), so "Doe, - #: Dr. nee Smith MA" reads given ``nee``, middle ``Smith`` and - #: suffix ``MA`` -- and reports, the ``MA`` ending a given part + #: Dr. nee Smith ma" reads given ``nee``, middle ``Smith`` and + #: suffix ``ma`` -- and reports, the ``ma`` ending a given part #: like any other. #: And fourth -- pre-existing and untouched by 2.4 as well -- #: a JOIN beside the member can @@ -542,20 +549,21 @@ class AmbiguityKind(StrEnum): #: it: "Doe, John van Ma" reads middle ``van Ma`` in silence, and #: so does "Doe, John DO Ed", where the member is itself the #: particle the chain runs on and the name word behind it joins - #: the piece -- though "Doe, John DO" alone reads the credential - #: and reports. The particle chain is the commonest joiner but not - #: the only one: the bound-given join takes a member into its pair - #: the same way, so "Berg, abdul MA" reads given ``abdul MA`` in - #: silence. Where a particle the suffix vocabulary does not + #: the piece -- though "Doe, John DO" alone reads the credential, + #: its capitals settling it. The particle chain is the commonest + #: joiner but not the only one: the bound-given join takes a member + #: into its pair the same way, so "Berg, abdul ma" reads given + #: ``abdul ma`` in silence; neither joins a member its capitals + #: settle ("Berg, abdul MA" reads suffix ``MA``). Where a particle the suffix vocabulary does not #: also claim as an unambiguous word stands BEHIND it, the given part ends at #: that particle as this walk reads it, and the attachment that #: moves the particle to the family runs a stage too late to - #: re-open the question -- so "Doe, John MA do" keeps middle - #: ``MA``, capitals and all, beside family ``do Doe``, reporting - #: only the attachment's own ``PARTICLE_OR_GIVEN``. Neither half - #: is a rule about particles as such, and the caps spellings show - #: it: "Doe, John van MA" reads family ``van Doe``, suffix ``MA`` - #: and reports both forks, and "Doe, John MA vd" reads suffix + #: re-open the question -- so "Doe, John ma do" keeps middle + #: ``ma`` beside family ``do Doe``, reporting only the + #: attachment's own ``PARTICLE_OR_GIVEN``. Neither half is a rule + #: about particles as such, and the caps spellings show it: "Doe, + #: John van MA" reads family ``van Doe``, suffix ``MA``, reporting + #: the particle's fork alone, and "Doe, John MA vd" reads suffix #: ``MA`` past a ``vd`` the suffix vocabulary claims outright. #: All four POSITIONS above are silent -- those last two names #: are the boundary each one stops at, not instances of it. @@ -568,7 +576,7 @@ class AmbiguityKind(StrEnum): #: ``Smith Ma`` and report nothing. The writer drew the boundary, #: so no fork was available to decline -- a settled position #: rather than a fork left un-asked. The boundary cuts both ways: - #: "Jane Doe (nee Smith) MA" gives suffix ``MA`` and reports, the + #: "Jane Doe (nee Smith) ma" gives suffix ``ma`` and reports, the #: member standing outside the span. SUFFIX_OR_NAME = "suffix-or-name" #: An input the title peel eats down to one last word which is diff --git a/tests/v2/cases.py b/tests/v2/cases.py index 1319edf42..10def9094 100644 --- a/tests/v2/cases.py +++ b/tests/v2/cases.py @@ -7091,14 +7091,63 @@ def _check_cjk_shape_purity(self) -> None: "An earlier round of #533 released it and read given 'abdul MA' " "in silence"), Case("the_bound_given_joins_control_without_the_clause", - "Berg, abdul MA", - {"given": "abdul MA", "family": "Berg"}, + "Berg, abdul ma", + {"given": "abdul ma", "family": "Berg"}, classification="parity", ambiguities=(), notes="the recorded control for the row above -- what the " "released member WOULD have read as, and why releasing " - "it buys nothing. 1.4.0 read it identically (first " - "'abdul MA', last 'Berg'). Unchanged by this rule", + "it buys nothing. Written in lower case since #785: in " + "capitals in a name of mixed case the member is settled " + "and P5 declines it (the row below). 1.4.0 read it " + "identically (first 'abdul ma', last 'Berg')", + shape=2), + Case("a_capitals_member_opening_the_part_reports_nothing_beside_one_that_does", + "Smith, MA PhD Ma", + {"family": "Smith", "suffix": "MA PhD Ma"}, + classification="fix(#785)", + ambiguities=("suffix-or-name",), + notes="C11: the opening 'MA' is settled by its own " + "capitals (S6) and reports nothing, though the " + "Title-case 'Ma' behind PhD is read by S7's company and " + "reports; the review of #785 found the first version " + "still reporting 'MA' wherever another member in the " + "part was not settled. 2.6.0 reported both, and 1.4.0 " + "read first 'MA', suffix 'PhD, Ma'"), + Case("a_capitals_member_ends_the_chain_after_a_title_particle_head", + "Smith, Freiherr von Berg MA Jones", + {"title": "Freiherr", "given": "von Berg", "family": "Smith", + "suffix": "MA Jones"}, + classification="fix(#785)", + ambiguities=("suffix-or-name",), + notes="rules/P2's chain stops at a member its capitals settle, " + "as it stops at a suffix word, so the member starts the " + "credential's run (S8) and the given part reads as " + "'Smith, Freiherr von Berg M.A. Jones' does; the first " + "version of #785 widened the run start but not the " + "chain's stop, and the chain took 'MA Jones' into the " + "given name in silence. 2.6.0 read given 'von Berg MA " + "Jones', and 1.4.0 first 'von Berg', middle 'Jones', " + "suffix 'MA'"), + Case("a_capitals_member_behind_a_lone_bound_given_name", + "Abdul MA", + {"given": "Abdul", "suffix": "MA"}, + classification="fix(#785)", + notes="S6: the capitals settle 'MA' with no words to spare, as " + "in 'Jack MA', and nothing reports it; the bound given " + "name is claimed by its vocabulary, so O5 has no " + "convention to report either, and the name carries no " + "report though it has no family. 2.6.0 read the same " + "fields and reported suffix-or-name"), + Case("the_bound_given_join_declines_a_capitals_member", + "Berg, abdul MA", + {"given": "abdul", "family": "Berg", "suffix": "MA"}, + classification="fix(#785)", + notes="rules/P5's join declines a member its capitals settle " + "as it declines a suffix word (S6), so 'MA' is the " + "credential, as 'Berg, abdul M.A.' reads, and nothing " + "reports it. 1.4.0, 2.0.0 and 2.6.0 read given " + "'abdul MA'", shape=2), Case("a_particle_chain_would_take_the_released_member", "Berg, Jane van der nee Smith DO", diff --git a/tests/v2/rules_budget.json b/tests/v2/rules_budget.json index 315d8206e..2ec80da19 100644 --- a/tests/v2/rules_budget.json +++ b/tests/v2/rules_budget.json @@ -39,11 +39,11 @@ "O5": {"words": 478, "then_words": 160, "because_sentences": 2, "except": 6, "precedence": 1, "accepted": 0, "accepted_words": 0}, "O6": {"words": 572, "then_words": 138, "because_sentences": 2, "except": 5, "precedence": 4, "accepted": 7, "accepted_words": 244}, "P1": {"words": 415, "then_words": 287, "because_sentences": 2, "except": 0, "precedence": 1, "accepted": 4, "accepted_words": 319}, - "P2": {"words": 270, "then_words": 234, "because_sentences": 1, "except": 0, "precedence": 0, "accepted": 3, "accepted_words": 167}, + "P2": {"words": 295, "then_words": 259, "because_sentences": 1, "except": 0, "precedence": 0, "accepted": 3, "accepted_words": 167}, "P3": {"words": 639, "then_words": 598, "because_sentences": 1, "except": 0, "precedence": 0, "accepted": 0, "accepted_words": 0}, "P4": {"words": 190, "then_words": 82, "because_sentences": 1, "except": 1, "precedence": 0, "accepted": 0, "accepted_words": 0}, - "P5": {"words": 685, "then_words": 399, "because_sentences": 1, "except": 2, "precedence": 3, "accepted": 3, "accepted_words": 198}, - "P6": {"words": 974, "then_words": 644, "because_sentences": 2, "except": 2, "precedence": 4, "accepted": 6, "accepted_words": 459}, + "P5": {"words": 705, "then_words": 399, "because_sentences": 1, "except": 2, "precedence": 3, "accepted": 3, "accepted_words": 198}, + "P6": {"words": 1034, "then_words": 651, "because_sentences": 2, "except": 2, "precedence": 5, "accepted": 6, "accepted_words": 459}, "P7": {"words": 138, "then_words": 64, "because_sentences": 3, "except": 0, "precedence": 0, "accepted": 1, "accepted_words": 28}, "P8": {"words": 515, "then_words": 450, "because_sentences": 2, "except": 0, "precedence": 0, "accepted": 2, "accepted_words": 309}, "P9": {"words": 405, "then_words": 265, "because_sentences": 1, "except": 2, "precedence": 0, "accepted": 0, "accepted_words": 0}, @@ -56,10 +56,10 @@ "S2": {"words": 657, "then_words": 119, "because_sentences": 1, "except": 0, "precedence": 7, "accepted": 3, "accepted_words": 214}, "S3": {"words": 960, "then_words": 398, "because_sentences": 3, "except": 4, "precedence": 5, "accepted": 6, "accepted_words": 343}, "S4": {"words": 306, "then_words": 164, "because_sentences": 2, "except": 0, "precedence": 3, "accepted": 0, "accepted_words": 0}, - "S5": {"words": 719, "then_words": 400, "because_sentences": 1, "except": 2, "precedence": 1, "accepted": 0, "accepted_words": 0}, + "S5": {"words": 731, "then_words": 400, "because_sentences": 1, "except": 2, "precedence": 1, "accepted": 0, "accepted_words": 0}, "S6": {"words": 580, "then_words": 325, "because_sentences": 2, "except": 1, "precedence": 2, "accepted": 0, "accepted_words": 0}, "S7": {"words": 602, "then_words": 388, "because_sentences": 1, "except": 2, "precedence": 2, "accepted": 1, "accepted_words": 105}, - "S8": {"words": 467, "then_words": 302, "because_sentences": 1, "except": 1, "precedence": 2, "accepted": 1, "accepted_words": 60}, + "S8": {"words": 467, "then_words": 302, "because_sentences": 1, "except": 1, "precedence": 2, "accepted": 1, "accepted_words": 84}, "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": 474, "then_words": 266, "because_sentences": 3, "except": 1, "precedence": 1, "accepted": 2, "accepted_words": 107}, diff --git a/tests/v2/test_cases.py b/tests/v2/test_cases.py index f13738a68..ccd5beed1 100644 --- a/tests/v2/test_cases.py +++ b/tests/v2/test_cases.py @@ -256,9 +256,10 @@ def test_the_field_sweep_sees_the_claims_it_checks() -> None: # takes the claims of the rows that reported one (each under # the orders it reaches); its new rows add their run reports, and # the rows rewritten in one case or lower case to keep reaching - # their emitter add theirs back. - assert claims == 1310, ( - f"the field sweep checks {claims} claims, recorded as 1310 on " + # their emitter add theirs back; 1310 -> 1316 the same day, the + # review's rows for the comma's opening word and the chain's stop. + assert claims == 1316, ( + f"the field sweep checks {claims} claims, recorded as 1316 on " f"2026-10-25") diff --git a/tools/differential/corpus_rules.jsonl b/tools/differential/corpus_rules.jsonl index 2811f5611..0707298d5 100644 --- a/tools/differential/corpus_rules.jsonl +++ b/tools/differential/corpus_rules.jsonl @@ -8,7 +8,7 @@ ", Dr. fils" ", Jane" ", Jane Mrs" -", MA" +", MA Smith" ", MD/PhD" ".," "1. née Smith" @@ -66,9 +66,11 @@ "Berg, Jan van; MD" "Berg, Jan vd" "Berg, abd née Jones" +"Berg, abdul MA" "Berg, abdul Prof." "Berg, abdul Prof. Ali" "Berg, abdul V" +"Berg, abdul ma" "Berg, abdul van" "Berg, abdul vd" "Berg, de; Smith" @@ -119,6 +121,7 @@ "Doe, John Msc.Ed." "Doe, John X.Y.Z." "Doe, John ma Ma" +"Doe, John ma do" "Doe, John van DO" "Doe, John van Ma" "Doe, MA PhD" @@ -311,6 +314,7 @@ "Jean Marie Fils" "Jean Pierre DUPONT" "Jean Pierre DUPONT-MORETTI" +"Jean Pierre MA DUPONT" "Jiří Dienstbier ml." "John . Smith" "John Doctor Smith" @@ -559,6 +563,7 @@ "Mary Jane King" "Mary Jane King Smith" "Mary Jane King." +"Mary Jane MA King" "Mary Smith née Jones Prof." "Mary-​Jane Smith" "Mc Donald" @@ -687,6 +692,7 @@ "Smith, Dr. Jr." "Smith, Esq." "Smith, Esq. John" +"Smith, Freiherr von Berg MA Jones" "Smith, Insp. John" "Smith, Jan van Prof." "Smith, Jane Mrs" diff --git a/tools/differential/corpus_shapes.jsonl b/tools/differential/corpus_shapes.jsonl index e2e68b237..bc2f44f97 100644 --- a/tools/differential/corpus_shapes.jsonl +++ b/tools/differential/corpus_shapes.jsonl @@ -141,6 +141,7 @@ {"name": "Berg, Jan mc", "shape": 2} {"name": "Berg, Jane van der nee Smith DO", "shape": 2} {"name": "Berg, abdul MA", "shape": 2} +{"name": "Berg, abdul ma", "shape": 2} {"name": "Berg, abdul nee Jones MA", "shape": 2} {"name": "Carod i Rovira, Josep", "shape": 2} {"name": "DOE, JOHN MA", "shape": 2} diff --git a/tools/differential/snapshot.jsonl b/tools/differential/snapshot.jsonl index b407a2c71..9c8e70e35 100644 --- a/tools/differential/snapshot.jsonl +++ b/tools/differential/snapshot.jsonl @@ -14,7 +14,7 @@ {"name": ", Jane", "given": "Jane", "initials": "J."} {"name": ", Jane Mrs", "title": "Mrs", "family": "Jane", "initials": "J."} {"name": ", John", "given": "John", "initials": "J."} -{"name": ", MA", "given": "MA", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "M.", "capitalized": "Ma"} +{"name": ", MA Smith", "given": "MA", "middle": "Smith", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "M. S.", "capitalized_force": "Ma Smith"} {"name": ", MD/PhD", "suffix": "MD/PhD"} {"name": ".,"} {"name": "1 & 2, 3 4 5, Mr.", "title": "Mr.", "given": "3", "middle": "4 5", "family": "1 & 2", "initials": "3. 4. 5. 1. 2."} @@ -354,10 +354,11 @@ {"name": "Berg, Jan vd", "given": "Jan", "family": "vd Berg", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. B."} {"name": "Berg, Jane van der nee Smith DO", "given": "Jane", "middle": "van der nee Smith", "family": "Berg", "suffix": "DO", "initials": "J. n. S. B.", "capitalized_force": "Jane van der Nee Smith Berg DO"} {"name": "Berg, abd née Jones", "given": "abd", "middle": "née Jones", "family": "Berg", "initials": "a. n. J. B.", "capitalized_force": "Abd Née Jones Berg"} -{"name": "Berg, abdul MA", "given": "abdul MA", "family": "Berg", "initials": "a. M. B.", "capitalized_force": "Abdul Ma Berg"} +{"name": "Berg, abdul MA", "given": "abdul", "family": "Berg", "suffix": "MA", "initials": "a. B.", "capitalized_force": "Abdul Berg MA"} {"name": "Berg, abdul Prof.", "title": "Prof.", "given": "abdul", "family": "Berg", "initials": "a. B.", "capitalized_force": "Prof. Abdul Berg"} {"name": "Berg, abdul Prof. Ali", "given": "abdul Prof.", "middle": "Ali", "family": "Berg", "initials": "a. P. A. B.", "capitalized_force": "Abdul Prof. Ali Berg"} {"name": "Berg, abdul V", "given": "abdul V", "family": "Berg", "initials": "a. V. B.", "capitalized_force": "Abdul V Berg"} +{"name": "Berg, abdul ma", "given": "abdul ma", "family": "Berg", "initials": "a. m. B.", "capitalized_force": "Abdul Ma Berg"} {"name": "Berg, abdul nee Jones MA", "given": "abdul", "middle": "nee Jones", "family": "Berg", "suffix": "MA", "initials": "a. n. J. B.", "capitalized_force": "Abdul Nee Jones Berg MA"} {"name": "Berg, abdul van", "given": "abdul van", "family": "Berg", "initials": "a. v. B.", "capitalized_force": "Abdul Van Berg"} {"name": "Berg, abdul vd", "given": "abdul", "family": "vd Berg", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "a. B.", "capitalized_force": "Abdul vd Berg"} @@ -1514,6 +1515,7 @@ {"name": "Jean Marie Fils", "given": "Jean", "middle": "Marie", "family": "Fils", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. M. F."} {"name": "Jean Pierre DUPONT", "given": "Jean", "middle": "Pierre", "family": "DUPONT", "initials": "J. P. D.", "capitalized_force": "Jean Pierre Dupont"} {"name": "Jean Pierre DUPONT-MORETTI", "given": "Jean", "middle": "Pierre", "family": "DUPONT-MORETTI", "initials": "J. P. D.", "capitalized_force": "Jean Pierre Dupont-Moretti"} +{"name": "Jean Pierre MA DUPONT", "given": "Jean", "family": "Pierre", "suffix": "MA DUPONT", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. P.", "capitalized_force": "Jean Pierre MA Dupont"} {"name": "Jean St Clair", "given": "Jean", "family": "St Clair", "initials": "J. C.", "capitalized_force": "Jean st Clair"} {"name": "Jean de Beaumanoir de Lavardin", "given": "Jean", "middle": "de Beaumanoir", "family": "de Lavardin", "initials": "J. B. L."} {"name": "Jean de Mesnil", "given": "Jean", "family": "de Mesnil", "initials": "J. M."} @@ -2388,6 +2390,7 @@ {"name": "Mary Jane King", "given": "Mary", "middle": "Jane", "family": "King", "initials": "M. J. K."} {"name": "Mary Jane King Smith", "given": "Mary", "middle": "Jane King", "family": "Smith", "initials": "M. J. K. S."} {"name": "Mary Jane King.", "title": "King.", "given": "Mary", "family": "Jane", "initials": "M. J."} +{"name": "Mary Jane MA King", "title": "King", "given": "Mary", "family": "Jane", "suffix": "MA", "initials": "M. J."} {"name": "Mary Johnson", "given": "Mary", "family": "Johnson", "initials": "M. J."} {"name": "Mary Nicet", "given": "Mary", "family": "Nicet", "initials": "M. N."} {"name": "Mary Smith née Jones Prof.", "title": "Prof.", "given": "Mary", "family": "Smith", "maiden": "Jones", "initials": "M. S."} @@ -3100,6 +3103,7 @@ {"name": "Smith, E.T., Jr.", "given": "E.T.", "family": "Smith", "suffix": "Jr.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "E. S."} {"name": "Smith, Esq.", "family": "Smith", "suffix": "Esq.", "initials": "S."} {"name": "Smith, Esq. John", "family": "Smith", "suffix": "Esq. John", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "S."} +{"name": "Smith, Freiherr von Berg MA Jones", "title": "Freiherr", "given": "von Berg", "family": "Smith", "suffix": "MA Jones", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "v. B. S.", "capitalized_force": "Freiherr Von Berg Smith MA Jones"} {"name": "Smith, I.", "given": "I.", "family": "Smith", "initials": "I. S."} {"name": "Smith, Insp. John", "title": "Insp.", "given": "John", "family": "Smith", "initials": "J. S."} {"name": "Smith, J. R.", "given": "J.", "middle": "R.", "family": "Smith", "initials": "J. R. S."} From e6c26910503d70ed5d7114820478374903926c23 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Sun, 25 Oct 2026 18:12:27 -0700 Subject: [PATCH 3/6] Narrow the chain stop and the join's decline to the family-comma given part (#785) The review of the fix commit found the chain stop stranding a settled member where the trailing read then declined it: `Freiherr von Berg MA Ma` read given 'von Berg', middle 'MA', family 'Ma' (given 'MA' under the family-first orders), away from both master and the dotted spelling, on 162 parses of its grid. P5's decline did the same with no comma (`Abdul MA Smith` -> middle 'MA' in silence). After a family comma the member always has a reader, the given part's trailing slot or the run S8 starts behind the given word, so both widenings stand there and nowhere else. Everywhere else the readings are master's: every name the narrowing moves on the review's grid returns to master's reading. Rules P2 and P5, decisions/P2, P5 and S6, and two case rows for the shapes the narrower checks keep. Co-Authored-By: Claude Opus 5.5 --- .../decisions/P2-particles-join-forward.md | 2 +- docs/design/decisions/P5-bound-given-names.md | 2 +- .../S6-case-signal-at-the-suffix-slot.md | 1 + .../design/rules/P2-particles-join-forward.md | 3 ++- docs/design/rules/P5-bound-given-names.md | 3 ++- nameparser/_pipeline/_group.py | 14 +++++++---- nameparser/_pipeline/_pieces.py | 16 +++++++++---- tests/v2/cases.py | 23 +++++++++++++++++++ tests/v2/rules_budget.json | 4 ++-- tools/differential/corpus_rules.jsonl | 2 ++ tools/differential/snapshot.jsonl | 2 ++ 11 files changed, 56 insertions(+), 16 deletions(-) diff --git a/docs/design/decisions/P2-particles-join-forward.md b/docs/design/decisions/P2-particles-join-forward.md index 04a4d2c30..4d86e2d13 100644 --- a/docs/design/decisions/P2-particles-join-forward.md +++ b/docs/design/decisions/P2-particles-join-forward.md @@ -35,4 +35,4 @@ Open: none in this file. ROWS: five rows pinned the old decision and now read the new one (`fix(#624)`, each note giving the earlier reading): `Freiherr St van Berg MA`, `Freiherr Freiherr Prof do`, `St St VI`, `Freiherr St MA`, `Freiherr St Prof.`; `Freiherr St John Smith MA`, the issue's example, is added, and `Dr. Mc Mc` pins the give-back half (the review found that half unpinned, a revert passing the suite). The marker row that held group's particle-or-given emitter on `St St née` moves to `St van née`, the same structure with a chained particle that is not a title (parity), since `St St née` no longer reaches that emitter. - 2026-10-19 #760 — MOVED FROM the chain's code comment in `_group.py`, the facts the entries above did not hold. LINEAGE: the chain is v1's prefix chain — a non-leading prefix run absorbs everything to the next prefix or suffix — with v1's `leading_first_name` rule keeping the first piece a name (`Van Johnson`). #367's SHAPE, beside the `Sir de Mesnil` entry above: keyed on index 0, a title displaced the particle and the chain fired, so identical name text parsed two ways — `Van Johnson` given 'Van', family 'Johnson'; `Dr. Van Johnson` family 'Van Johnson'. SUFFIX PIECES ARE NOT SKIPPED when the chain looks for the leading piece, and the reason is what skipping them would do rather than what it would cost. The shapes that decide it are those whose leading piece lands in `given`: `Ph.D. Van Johnson`, `II Van Johnson` and `Msc.Ed. Van Johnson` each read given 'Ph.D.'/'II'/'Msc.Ed.' with family 'Van Johnson', and skipping the piece moves `Van` out of the family into the middle name (given 'Ph.D.', middle 'Van', family 'Johnson') — a worse reading on three shapes, fixing none. `Jr. Van Johnson`, the shape that looks as if it needs the skip, classifies its leading piece as a TITLE and is already covered by the title run. The example that opened this argument, `Ph. D. Van Johnson`, left it with #371 (the pair no longer merges at the head of the input, so there is no suffix-shaped leading piece to skip); `Dr. Ph. D. Van Johnson` replaces it — family 'Van Johnson' as shipped, family 'Johnson' with the skip. -- 2026-10-25 #785 — THE CHAIN STOPS AT A MEMBER ITS CAPITALS SETTLE, as at a suffix word, wherever it stands: the trailing read already ended the chain at one ending the name (decisions/S6's `Freiherr von Berg MA`), and a settled member with name words behind it, which the chain took into its run, now starts S8's credential run instead (`Smith, Freiherr von Berg MA Jones` → given 'von Berg', suffix 'MA Jones', as its dotted spelling reads). Found by #785's code review; the record is decisions/S6's review entry of this date. +- 2026-10-25 #785 — IN THE GIVEN PART AFTER A FAMILY COMMA THE CHAIN STOPS AT A MEMBER ITS CAPITALS SETTLE, as at a suffix word, since the run S8 starts behind the given word takes it and the words behind it (`Smith, Freiherr von Berg MA Jones` → given 'von Berg', suffix 'MA Jones', as its dotted spelling reads), where the chain had taken them into the given name. Elsewhere the trailing read's bound already ends the chain at a member the read takes (decisions/S6's `Freiherr von Berg MA`), and a stop at one the read declines strands it: the review of the fix found `Freiherr von Berg MA Ma` reading given 'von Berg', middle 'MA', family 'Ma' under a stop everywhere, so no-comma names keep the chain's reading (family 'von Berg MA Ma', where the dotted spelling reads suffix 'M.A. Ma'). Found by #785's code review; the record is decisions/S6's review entries of this date. diff --git a/docs/design/decisions/P5-bound-given-names.md b/docs/design/decisions/P5-bound-given-names.md index 0ba6f92b5..512e0ce89 100644 --- a/docs/design/decisions/P5-bound-given-names.md +++ b/docs/design/decisions/P5-bound-given-names.md @@ -60,4 +60,4 @@ Open: none in this file. DECIDED (Derek): this narrower clause (a′) rather than declining every title word (a). P5 states that a title word standing in the name is a name word and joins like one (H3), and (a) undid it: it failed six tests, a P5 rules example and the two tests named for that clause among them, and over the bound-given grid below moved 4,044 parses, 3,000 of them a join split with no title read (`(Bob), Abu Bishop` → given 'Abu', middle 'Bishop') beside the 1,044 that (a′) moves (six tests measured against 4455962f's suite). (a′) leaves every join on a title word inside the name standing -- `abdul Sir Smith Berg`, `Berg, abdul Prof. Ali`, `Smith, abdul Bishop`. MEASURED 2026-10-11 against 4455962f (#639's merge; this change is PR #643), over a 5,184-text bound-given grid (twelve heads × `abdul`, `Abu`, `abd al` × sixteen next words × nine tails) under the three orders: 1,044 parses move, every one a title word into the title field, 660 of them in a part past the second comma; #623's given-part grid moves 280, 260 of the same class and 20 the declined join below; no corpus name or case text but `Berg, abdul Prof.`, and nothing on the S2, #624 or #625 grids; the five gates exit 0; frames unmoved on the reference rows, up to `Berg, abdul Prof.` 207 → 228 on rare shapes no guard sees; recorded negative controls, 11 failing tests with no title declined and 5 with the past-the-second-comma half removed. THE REVIEW (2026-10-11). The decline after a family comma asks H5's run as the comma-less name reads it (`tail_reading`), and the given part's own walk reads some words behind a title otherwise, the disagreement #623's entry (decisions/H5) records for the chain's bound: there the join is declined and no title is read. A shape-only numeral VI to X, or V before a third comma part, which the walk keeps a name word (`Smith, abdul Prof. VI` → given 'abdul', middle 'Prof. VI', where master and 1.4.0 read given 'abdul Prof.', middle 'VI'); a period-marked title that is also a particle, which P6 then attaches with its report (`Smith, abdul St.` → family 'St. Smith', particle-or-given); and a bound word that is also a particle before one (`Smith, Abu St.` → title 'St.', where `Smith, Zed St.` reads family 'St. Smith'). Every such shape is garbage; the first two read as the same name with an ordinary given word does. Recorded, not narrowed, as #623's disagreement was (Derek, #623), and rules/P5 states it as an Accepted clause; the review's sweep found nothing reading worse than master on a real name and no comma-less name moving. It also found two mechanisms no test held, now rows: the bare-title half past the second comma (`John Smith, Jr., abdul Sir Smith`) and the whole trailing run after a family comma rather than the last word (`Berg, abdul Prof. Dr.`). -- 2026-10-25 #785 — THE JOIN DECLINES A MEMBER ITS CAPITALS SETTLE, as it declines a word of the unambiguous suffix vocabulary: in a name of mixed case a listed member of the ambiguous class written in capitals is the credential (rules/S6), so `Smith, Abdul MA` reads given 'Abdul', suffix 'MA', where 1.4.0 through 2.6.0 read given 'Abdul MA' (`Smith, abdul ma` still joins). Found by #785's code review; the record is decisions/S6's review entry of this date. +- 2026-10-25 #785 — AFTER A FAMILY COMMA THE JOIN DECLINES A MEMBER ITS CAPITALS SETTLE, as it declines a word of the unambiguous suffix vocabulary: in a name of mixed case a listed member of the ambiguous class written in capitals is the credential (rules/S6), and the given part's trailing slot or S8's run then takes it, so `Smith, Abdul MA` reads given 'Abdul', suffix 'MA', where 1.4.0 and 2.6.0 read given 'Abdul MA' (`Smith, abdul ma` still joins). With no comma the join stands (`Abdul MA Smith` → given 'Abdul MA'): declined there, nothing takes the member, which reads as a middle name in silence, as the review of the fix found. Found by #785's code review; the record is decisions/S6's review entry of this date. diff --git a/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md b/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md index 98822787b..8f42b3cb1 100644 --- a/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md +++ b/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md @@ -42,3 +42,4 @@ Open: none in this file. FRAMES (`sys.setprofile` call events over a second parse, py3.11): the reference row and plain names cost what they did (`Dr. Juan0001 de la Vega III` 305, `John Smith` 149, `Smith, John` 158, `John Smith, PhD` 202); a name holding a member pays for the predicate where it is asked (`John Smith Ma` 213 → 220, `JOHN SMITH MA` 212 → 215, `John Smith MA` 212 → 203, the pick and its report gone) and for the run it now reads (`John Smith MA Jones` 232 → 259). Recompute: `uv run python tools/differential/snapshot.py --since `; for each local corpus `snapshot.py --local --base `, and `local_agreement.py psc-individuals --base `; the ctgov shape count by reading `corpus.jsonl` of ctgov-officials, keeping each distinct name whose part before its first comma holds, after two words, a word whose letters are a listed member written in capitals between two words that each open with a capital and are not wholly in capitals, and counting those whose suffix holds it, on this tree and on the parent. - 2026-10-25 #785, review — THREE SITES THE FIRST VERSION LEFT ASKING THE OLD QUESTION, found by the PR's code review on a grid of 104,676 parses (125 shapes, 23 spellings of the member as written, in upper and in lower case, three orders, four policies) against master, and a twin grid comparing each capitals member with its dotted spelling. (i) The comma's report on the part's opening word was withheld only where every listed member of the part was settled, so `Smith, MA PhD Ma` still reported its settled `MA` beside the Title-case `Ma` S7's company reads; it asks the word's own writing now. (ii) P2's chain stopped at a suffix word and not at a settled member, so after a family comma with a title-particle head the chain took the member and the words behind it into the given name in silence (`Smith, Freiherr von Berg MA Jones` → given 'von Berg MA Jones', where `Smith, Freiherr von Berg M.A. Jones` reads suffix 'M.A. Jones'); `_pieces.chain_run_end` asks `settled_by_case` now, the chain's units with it, and the member starts the run. (iii) P5's bound given-name join took a settled member as the given name's second word (`Smith, Abdul MA` → given 'Abdul MA' in silence, where `Smith, Abdul M.A.` reads suffix 'M.A.'); it declines one now, as it declines a suffix word (decisions/P5). Measured after the fixes, against the same master: the twin grid's capitals-against-dotted disagreements fall from 4,062 on master to 2,250, every one of which master shares; the main grid's removed reports all name a settled member and no report is added; ctgov-officials, fec-individuals and tse-donors move exactly as the entry above records, the three sites reaching no name in them. The design-docs review found the API reference (`AmbiguityKind.SUFFIX_OR_NAME`'s docstring), rules/P6 and Standing lines in decisions/M2, S4 and S5 still stating the old reports, and S8's Accepted clause narrower than its cost: a family name written in capitals by convention reads the same way (`Jean Pierre MA DUPONT` → suffix 'MA DUPONT'), and a name word behind the member that is title vocabulary reads as a title in silence (`Mary Jane MA King` → title 'King', as `Mary Jane M.A. King` reads); all are amended. Left as it reads: `John Smith MA y` reads family 'MA y' where `John Smith PhD y` reads suffix 'PhD y', a trailing connective no writing produces. +- 2026-10-25 #785, review of the fix — THE CHAIN STOP AND THE JOIN'S DECLINE ARE NARROWED TO THE GIVEN PART AFTER A FAMILY COMMA. SUPERSEDES the review entry's (ii) and (iii) where they read "wherever". Applied everywhere, both left a settled member where nothing then took it, read as a name word in silence: the chain stopped in front of a member the trailing read declined (`Freiherr von Berg MA Ma` → given 'von Berg', middle 'MA', family 'Ma', and under the family-first orders given 'MA'; 162 parses of the fix review's grid, the only moves of the fix away from both master and the dotted spelling), and the join declined one with no comma (`Abdul MA Smith` → middle 'MA'). After a family comma the member always has a reader, the given part's trailing slot or the run S8 starts behind the given word, so the two widenings stand there and nowhere else; outside it the readings are master's (decisions/P2, decisions/P5). The comma's opening-word guard was found sound. diff --git a/docs/design/rules/P2-particles-join-forward.md b/docs/design/rules/P2-particles-join-forward.md index daa7c3fb9..560e6c4f4 100644 --- a/docs/design/rules/P2-particles-join-forward.md +++ b/docs/design/rules/P2-particles-join-forward.md @@ -8,9 +8,10 @@ ``` "Dr. John van der Berg" → family="van der Berg" ``` -- **Where a trailing suffix begins** is the trailing run S2 reads, once, before the chain is made and counting the chain's own run as the one name word it will be, so the chain cannot change what it counts: a trailing roman numeral S10 reads as the generation, or a bare acronym with words to spare, ends the chain as a suffix word does, and so does a bare ambiguous acronym written in capitals in a mixed-case name (S6), which needs no words to spare, wherever it stands in the name: settled by its writing, it is a suffix word to the chain, and the run it starts is S8's. +- **Where a trailing suffix begins** is the trailing run S2 reads, once, before the chain is made and counting the chain's own run as the one name word it will be, so the chain cannot change what it counts: a trailing roman numeral S10 reads as the generation, or a bare acronym with words to spare, ends the chain as a suffix word does, and so does a bare ambiguous acronym written in capitals in a mixed-case name (S6), which needs no words to spare. In the given part after a family comma such a member ends the chain wherever it stands, since the run S8 starts behind the given word takes it and every word behind it; elsewhere a member the trailing read does not take stays the chain's. ``` "Smith, Freiherr von Berg MA Jones" → given="von Berg" + "Freiherr von Berg MA Ma" → family="von Berg MA Ma" · boundary "John van der Berg PhD" → family="van der Berg" "John van der Berg V" → family="van der Berg" "John van der Berg V" → suffix="V" diff --git a/docs/design/rules/P5-bound-given-names.md b/docs/design/rules/P5-bound-given-names.md index a25d293ef..6a786b076 100644 --- a/docs/design/rules/P5-bound-given-names.md +++ b/docs/design/rules/P5-bound-given-names.md @@ -51,10 +51,11 @@ **Except:** -- **The join never absorbs a marker standing as a word of its own** — a marker is not a name word (M2) — nor a word of the unambiguous suffix vocabulary (S2), wherever position will then place it, nor a member of the ambiguous class its capitals settle as the credential in a name of mixed case (S6), nor a credential split across a space (S12) that the given part after a family comma holds, nor a title word a trailing rule reads as a title: one the trailing title run (H5) takes from the given part after a family comma, read as the comma-less name reads it with the bare salutations H8 adds, or any title word in a part past the second comma (C2). +- **The join never absorbs a marker standing as a word of its own** — a marker is not a name word (M2) — nor a word of the unambiguous suffix vocabulary (S2), wherever position will then place it, nor, after a family comma, a member of the ambiguous class its capitals settle as the credential in a name of mixed case (S6), which the given part's own reading then takes, nor a credential split across a space (S12) that the given part after a family comma holds, nor a title word a trailing rule reads as a title: one the trailing title run (H5) takes from the given part after a family comma, read as the comma-less name reads it with the bare salutations H8 adds, or any title word in a part past the second comma (C2). ``` "Berg, abdul MA" → suffix="MA" "Berg, abdul ma" → given="abdul ma" + "Abdul MA Smith" → given="Abdul MA" · boundary "abdul Jr Smith Berg" → given="abdul" "abdul Jr Smith Berg" → middle="Jr Smith" "abdul Ph. D. Smith Berg" → suffix="Ph. D." diff --git a/nameparser/_pipeline/_group.py b/nameparser/_pipeline/_group.py index 32750c813..478b4945b 100644 --- a/nameparser/_pipeline/_group.py +++ b/nameparser/_pipeline/_group.py @@ -961,7 +961,8 @@ def chain(tail: int) -> None: k += 1 continue j = chain_run_end(k, pieces, ptags, tokens, - len(pieces) - tail, one_case) + len(pieces) - tail, + one_case if given_slot else None) # The other half of PARTICLE_OR_GIVEN. _assign reports the # fork when an ambiguous particle stays a lone leading piece # ("Van Johnson" -> given under the default order, family @@ -1163,14 +1164,17 @@ def chain(tail: int) -> None: # as its one-word spelling is: merged in the given part # before this join, so the particle chain stops in front of # it too ('Rahman, Abdul MB ChB', 'Smith, John van Pharm D') - # A member its capitals settle as the credential (rules/S6) - # is declined as a suffix word is: the join would read it as - # the given name's second word ('Smith, Abdul MA') + # After a family comma a member its capitals settle as the + # credential (rules/S6) is declined as a suffix word is: the + # join would read it as the given name's second word, where + # the given part's trailing slot or the run S8 starts takes + # it ('Smith, Abdul MA'); with no comma nothing would, and + # the join stands ('Abdul MA Smith') nxt, nxt_tags = pieces[fk + 1], ptags[fk + 1] if (_is_maiden_marker_piece(nxt, tokens) or is_suffix_piece(nxt, nxt_tags, tokens) or "split" in nxt_tags - or (len(nxt) == 1 + or (bound_join is BoundJoin.LENIENT and len(nxt) == 1 and AMBIGUOUS_ACRONYM_TAG in tokens[nxt[0]].tags and settled_by_case(tokens[nxt[0]], one_case)) or ((site is ClauseSite.TAIL diff --git a/nameparser/_pipeline/_pieces.py b/nameparser/_pipeline/_pieces.py index 4ec5745f3..1cb0cad9e 100644 --- a/nameparser/_pipeline/_pieces.py +++ b/nameparser/_pipeline/_pieces.py @@ -2003,10 +2003,17 @@ def chain_run_end(k: int, pieces: Sequence[Sequence[int]], """Where the chain run opened by the particle `pieces[k]` ends: past the particles straight behind it, then past the name words up to `end`, a suffix piece, a member its capitals settle as the - credential (`settled_by_case`, with `one_case` ParseState's), or + credential where the caller passes `one_case` (ParseState's), or the next particle. `pieces[k:j]` is the one name unit P2 makes of them; `j == k + 1` where it makes none. + Only the given part after a family comma passes `one_case`: there + the member starts S8's run behind the given word whatever stands + in front, so the chain leaves it to the run. Elsewhere the trailing + read's `end` stops the chain at a member the read takes, and a + stop at one it declines would strand it as a name word between the + particle run and the words behind it (#785's review). + The particle test is `is_prefix_piece`'s definition written inline: the walk runs per piece of every chained name (AGENTS.md's frame budget, question 1), and so is the member tag ahead of the @@ -2085,8 +2092,7 @@ def merge_particle_runs(pieces: list[list[int]], ptags: list[set[str]], def _chain_units(pieces: Sequence[Sequence[int]], ptags: Sequence[Set[str]], tokens: Sequence[WorkToken], - n: int, lead: int | None = None, - one_case: bool | None = None) -> list[int] | None: + n: int, lead: int | None = None) -> list[int] | None: """The name units P2's chain will make of `pieces`, as a mark per piece -- OPENS where a piece opens a unit, and JOINED where the chain will join a name word to the run in front of it and the @@ -2119,7 +2125,7 @@ def _chain_units(pieces: Sequence[Sequence[int]], else lead) + 1 while k < count: if prefix[k]: - j = chain_run_end(k, pieces, ptags, tokens, count, one_case) + j = chain_run_end(k, pieces, ptags, tokens, count) q = k + 1 # a split credential is weighed as the class it is a # member of (rules/S12) @@ -2187,7 +2193,7 @@ def read_trailing_run(pieces: Sequence[Sequence[int]], mc mc' in the family.""" if n == len(pieces): return None - units = _chain_units(pieces, ptags, tokens, n, lead, one_case) + units = _chain_units(pieces, ptags, tokens, n, lead) rest, titled, peel = tail_reading(peel_walk(n, ptags), pieces, ptags, tokens, one_case, units) tail: set[int] = set() diff --git a/tests/v2/cases.py b/tests/v2/cases.py index 10def9094..ec20955bf 100644 --- a/tests/v2/cases.py +++ b/tests/v2/cases.py @@ -7129,6 +7129,29 @@ def _check_cjk_shape_purity(self) -> None: "given name in silence. 2.6.0 read given 'von Berg MA " "Jones', and 1.4.0 first 'von Berg', middle 'Jones', " "suffix 'MA'"), + Case("a_capitals_member_mid_name_stays_in_the_chain", + "Freiherr von Berg MA Ma", + {"title": "Freiherr", "family": "von Berg MA Ma"}, + classification="fix(#785)", + ambiguities=("particle-or-given", "suffix-or-name"), + notes="P2: outside the given part after a family comma the " + "chain is bounded by the trailing read alone, which " + "declines 'Ma' and so takes nothing, and the chain keeps " + "'MA Ma' as 2.6.0 did. The review of #785's fix found a " + "stop at the settled 'MA' here stranding it as a middle " + "name (given 'von Berg', middle 'MA', family 'Ma'); the " + "dotted 'Freiherr von Berg M.A. Ma' reads suffix 'M.A. " + "Ma', and 1.4.0 read first 'von Berg', last 'MA', suffix " + "'Ma'"), + Case("a_capitals_member_mid_name_keeps_the_bound_join", + "Abdul MA Smith", + {"given": "Abdul MA", "family": "Smith"}, + classification="fix(#785)", + notes="P5: with no comma the join stands, since declined here " + "the member would be read as a middle name in silence, " + "nothing behind it taking it (the review of #785's fix). " + "2.6.0 read the same; 1.4.0 read first 'Abdul', middle " + "'MA', last 'Smith'"), Case("a_capitals_member_behind_a_lone_bound_given_name", "Abdul MA", {"given": "Abdul", "suffix": "MA"}, diff --git a/tests/v2/rules_budget.json b/tests/v2/rules_budget.json index 2ec80da19..b1f80a087 100644 --- a/tests/v2/rules_budget.json +++ b/tests/v2/rules_budget.json @@ -39,10 +39,10 @@ "O5": {"words": 478, "then_words": 160, "because_sentences": 2, "except": 6, "precedence": 1, "accepted": 0, "accepted_words": 0}, "O6": {"words": 572, "then_words": 138, "because_sentences": 2, "except": 5, "precedence": 4, "accepted": 7, "accepted_words": 244}, "P1": {"words": 415, "then_words": 287, "because_sentences": 2, "except": 0, "precedence": 1, "accepted": 4, "accepted_words": 319}, - "P2": {"words": 295, "then_words": 259, "because_sentences": 1, "except": 0, "precedence": 0, "accepted": 3, "accepted_words": 167}, + "P2": {"words": 315, "then_words": 279, "because_sentences": 1, "except": 0, "precedence": 0, "accepted": 3, "accepted_words": 167}, "P3": {"words": 639, "then_words": 598, "because_sentences": 1, "except": 0, "precedence": 0, "accepted": 0, "accepted_words": 0}, "P4": {"words": 190, "then_words": 82, "because_sentences": 1, "except": 1, "precedence": 0, "accepted": 0, "accepted_words": 0}, - "P5": {"words": 705, "then_words": 399, "because_sentences": 1, "except": 2, "precedence": 3, "accepted": 3, "accepted_words": 198}, + "P5": {"words": 717, "then_words": 399, "because_sentences": 1, "except": 2, "precedence": 3, "accepted": 3, "accepted_words": 198}, "P6": {"words": 1034, "then_words": 651, "because_sentences": 2, "except": 2, "precedence": 5, "accepted": 6, "accepted_words": 459}, "P7": {"words": 138, "then_words": 64, "because_sentences": 3, "except": 0, "precedence": 0, "accepted": 1, "accepted_words": 28}, "P8": {"words": 515, "then_words": 450, "because_sentences": 2, "except": 0, "precedence": 0, "accepted": 2, "accepted_words": 309}, diff --git a/tools/differential/corpus_rules.jsonl b/tools/differential/corpus_rules.jsonl index 0707298d5..eca153a84 100644 --- a/tools/differential/corpus_rules.jsonl +++ b/tools/differential/corpus_rules.jsonl @@ -15,6 +15,7 @@ "A.B. Vajpayee" "AHMAD IBN-ʿALĪ" "ANH DO" +"Abdul MA Smith" "Abu Bakar" "Abu Bakar Salim" "Ahmad al-ʿAbbas" @@ -174,6 +175,7 @@ "É. y É. Smith" "Freiherr St John Smith" "Freiherr von Berg MA" +"Freiherr von Berg MA Ma" "Freiherr von Berg, Ed" "Freiherr von Richthofen V" "Gabriel García Márquez" diff --git a/tools/differential/snapshot.jsonl b/tools/differential/snapshot.jsonl index 9c8e70e35..64bc84c40 100644 --- a/tools/differential/snapshot.jsonl +++ b/tools/differential/snapshot.jsonl @@ -33,6 +33,7 @@ {"name": "Abd van der Berg", "given": "Abd", "family": "van der Berg", "initials": "A. B."} {"name": "Abd van der Berg née Jones", "given": "Abd", "family": "van der Berg", "maiden": "Jones", "initials": "A. B."} {"name": "Abdul Aziz Yassin", "given": "Abdul Aziz", "family": "Yassin", "initials": "A. A. Y."} +{"name": "Abdul MA Smith", "given": "Abdul MA", "family": "Smith", "initials": "A. M. S.", "capitalized_force": "Abdul Ma Smith"} {"name": "Abdul Razak Hussein", "given": "Abdul Razak", "family": "Hussein", "initials": "A. R. H."} {"name": "Abdul Salam Ahmed Salem, MD", "given": "Abdul Salam", "middle": "Ahmed", "family": "Salem", "suffix": "MD", "initials": "A. S. A. S."} {"name": "Abdul Salam Hassan, MD", "given": "Abdul Salam", "family": "Hassan", "suffix": "MD", "initials": "A. S. H."} @@ -1008,6 +1009,7 @@ {"name": "Frederic Viñals i Iscla", "given": "Frederic", "family": "Viñals i Iscla", "initials": "F. V. I."} {"name": "Freiherr St John Smith", "title": "Freiherr St", "given": "John", "family": "Smith", "initials": "J. S."} {"name": "Freiherr von Berg MA", "title": "Freiherr", "family": "von Berg", "suffix": "MA", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "B."} +{"name": "Freiherr von Berg MA Ma", "title": "Freiherr", "family": "von Berg MA Ma", "ambiguities": ["PARTICLE_OR_GIVEN", "SUFFIX_OR_NAME"], "initials": "B. M. M.", "capitalized_force": "Freiherr von Berg Ma Ma"} {"name": "Freiherr von Berg, Ed", "given": "Ed", "family": "Freiherr von Berg", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "E. B.", "capitalized_force": "Ed freiherr von Berg"} {"name": "Freiherr von Richthofen", "title": "Freiherr", "family": "von Richthofen", "ambiguities": ["PARTICLE_OR_GIVEN"], "initials": "R."} {"name": "Freiherr von Richthofen V", "title": "Freiherr", "family": "von Richthofen V", "ambiguities": ["PARTICLE_OR_GIVEN", "SUFFIX_OR_NAME"], "initials": "R. V."} From 46e59a250def3778d608d826ae85a36f97c896d2 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Mon, 26 Oct 2026 10:36:57 -0700 Subject: [PATCH 4/6] Keep a capitals credential a name word where a name word follows it (#785) Derek, on PR #895 (2026-10-26): narrow #785's second question rather than accept its cost. In a name with no family comma, a listed member its capitals settle no longer starts S8's credential run where a name word stands right behind it: a particle, or a Title-case or capitals word that is no credential. That word is evidence the capitals are the writer's initials, as the design's cue table has Title case lean to the name. `Maria Ana MA Silva`, `Jean Pierre MA DUPONT` and `Mary Jane MA King` keep the readings 1.4.0 through 2.6.0 gave; `John Smith MA Jones` keeping family 'Jones' is the new Accepted cost in S8. A credential behind the member still lets the run start (`John Smith MA JD Jones`), and after a family comma nothing changes. `run_start` asks `_name_word_behind` of a settled member; the given part's predictor passes `initials_cue=False` to keep agreeing with assign's walk. The first draft keyed the test on the class tag, which the caps switch writes on a shape-admitted numeral (`John Smith VI Ma` lost its generation reading under it); it asks `settled_by_case` now. ctgov moves 1,772 readings against master (1,767 dropped reports, 5 credential-list fixes); the 6 initials readings are gone. FEC, tse, PSC and the agreement records match. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- docs/customize.rst | 4 +- .../S6-case-signal-at-the-suffix-slot.md | 6 ++- .../decisions/S8-credentials-trailing-run.md | 3 +- docs/design/decisions/vocabulary-model.md | 3 +- .../S6-case-signal-at-the-suffix-slot.md | 2 +- .../rules/S8-credentials-trailing-run.md | 16 ++++--- docs/release_log.rst | 2 +- nameparser/_pipeline/_pieces.py | 43 ++++++++++++++++--- tests/v2/cases.py | 41 ++++++++++++++++++ tests/v2/pipeline/test_pieces.py | 13 +++++- tests/v2/rules_budget.json | 4 +- tests/v2/test_cases.py | 7 +-- tests/v2/test_facade_cases.py | 1 + tools/differential/corpus_rules.jsonl | 3 +- tools/differential/snapshot.jsonl | 11 ++--- 16 files changed, 127 insertions(+), 34 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 90563feb2..95b4b9625 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -186,7 +186,7 @@ The library has two layers: `nameparser/config/` (data) and `nameparser/parser.p **Sweep the forms a change can reach: comma shapes, then `name_order`.** No comma, a FULL name before the comma, a ONE-WORD name before the comma and NOTHING before the comma are four paths, and the last two are the misses — #429, #430 and #432 are all the one-word path disagreeing with the full-name path on inputs the full-name path reads correctly, and #613's PR review found `, PhD` and `, Jr.` (an empty surname field, as CSV data writes it) read as given and title by a decision that returned early on an empty head, after two review rounds whose every grid had a name before the comma. Put an empty head and a nickname-only head (`(Bob), PhD`) in every comma grid. Count the one-word path the way C3 counts, which is neither tokens nor pieces: a particle run and the one word it attaches to are one word (#575), a connective join and a bound given-name pair count as their words (#613, so `Ortega y Gasset, Dr.` is a whole name), and a suffix beside the surname counts as no name word at all. #603's review found the suffix half (`Vega y Lopez Jr., PhD Jones` lost its family where `Vega y Lopez, PhD Jones` kept it); #613 then reversed the connective half, and both now read given 'Vega', middle 'y', family 'Lopez' — so test a particle surname, a connective surname and a surname with a suffix, each before a comma. For orders the sweep already exists (`tests/v2/test_cases.py` runs every row under all three) but carries one assertion, R2's family partition, so it checks nothing a new change moves; coverage there has been vacuous before (PR #394's review found the suite passed with `name_order` discarded from grouping). Parse your change's names in each comma shape and each order, and read the ones you did not predict. And read a generated grid's moves by SHAPE, not by corpus membership: #614's first design passed the suite, the five gates and a fingerprint of every corpus name, because its grid moves were sorted into 'corpus name moved' and 'garbage', and the review found the moves that mattered among the 'garbage' -- a bare salutation (`Mr. and Mrs.` lost its connective to the family name) and a name with one trailing word (`Freiherr von Berg X.Y.Z.`). Pull out the head-plus-one-word slice of the grid and a salutation with nothing after it, and read every move there. Two more were the second review's (#614): no grid put TWO words that are both a title and a particle at the head (`Freiherr St van Berg MA`, where an index into the pieces was applied to a merged view of them -- `TITLES ∩ particles_ambiguous` is `{freiherr, st}`, so put both in every head list, each in its bare and its period-marked spelling -- #637's grid had `St` and `Freiherr` alone and missed that `St.` and `Freiherr.` are declined by the bound join and then attached by P6), and the grids compared ambiguity KINDS, so a report telling the caller the wrong field (`Freiherr von Berg Ma`, 'read as a given name' of a word in the family) passed every pin. #701's review added one: put a title/suffix dual (`Ms`, `Sr.`, `Cpl`) SECOND in a title run, behind an ordinary title. A stop that asks only "is this word suffix vocabulary" passes every row that opens with the dual and fails `Rev. Sr. Smith, Mary`; it was found by comparing `T Smith, John` against `T John Smith` over every title word, and a comma-versus-comma-less comparison is the cheap check for any rule that moves a word between the two slots. Compare each report's `detail` against the field its tokens land in -- since #626 `test_a_report_names_the_field_its_word_lands_in` (tests/v2/test_cases.py) does it over every non-locale case row under all three orders (a row carrying its own policy only as declared), for the phrasings its `_FIELD_CLAIMS` lists -- so a row with the shape is the test only where the report's wording is listed: a new emitter's wording goes in that list, and segment's report on a part past the second comma names no field (#629). #626's own first grid then missed an emitter for want of one comma shape (`, Ma Dr.`: an empty head, an ambiguous word, a trailing title, which H1 moves to the family): a trailing title belongs in every empty-head comma grid too. And #620's: an exclusion list in code being replaced is a set of rules, not noise -- #614's merged copy excluded every word the trailing read weighs from its units, the first #620 prototype dropped the list as a symptom of the copy, and its review found the list had been keeping positional counting's rule that a member in front counts as a word (`Freiherr von Berg MA X.Y.Z.` lost its suffix). Before deleting one, mutate each entry out and read what moves. The converse holds for replacing a hand copy WITH the shared predicate: it is a behavior change wherever the two disagreed, and #735's /simplify shipped one as "no reading moves" because the suite and the snapshot were its only check (`St Sir de Mesnil` family-first, a token test replaced by P1's piece test); diff the two predicates' answers over a grid before calling the swap a refactor. -**Then sweep the spellings: a class reached by vocabulary is also reached by SHAPE.** A test that uses only the listed spelling of a word class walks only the vocabulary path, and the shape path is a separate branch that can be missing while every test passes. Roman numerals are the sharpest case: `ii`, `iii` and `iv` are suffix vocabulary; `i` and `v` are vocabulary too but initial-shaped, so `is_suffix_piece` vetoes them and only the numeral fork reads them; and `vi` through `x` are in no list at all, read by the fork's shape test alone (checked 2026-10-04 against `Lexicon.default()`) -- which stops at `x`: `_vocab._ROMAN` matches I to X, so `John Smith XI` reads `XI` as a name word (re-checked 2026-10-07, #614's /simplify, which found a docstring claiming "any length"). And the fork counts a ONE-letter numeral besides, since #718 (rules/S10): it is the suffix only where the name has a word to spare, so `John V` and `John Smith V` take different branches of one fork, a one-case name being counted like capitals whatever the letter -- since #721 a one-case one-letter numeral carries the `initial` tag its capital does, and since #723 every one-case bare letter does (rules/T4), and a bare letter in front of the numeral is initial-shaped as its capital is, so `john v` reads as `JOHN V` does over `test_a_one_case_name_reads_as_its_capitals`'s grid (comma shapes, orders, a maiden clause, a particle chain, a bare letter in front, a trailing title, a non-ASCII letter in front: 315 of its then 1,872 pairs disagreed at 2bfa9533, before #721, and none after). Its first version claimed "every shape" with no bare letter in front and no trailing title in the grid, and #721's reviews found both broken; a grid's claim is the grid -- put a head-plus-numeral row beside every words-to-spare row. So a change that handles `V` and `III` can still miss `VI` — #610 found exactly that, the maiden take's candidate loop admitting a shape-only numeral in one position where the peel reads it in two (`Jane Doe nee Smith VI Prof.` kept maiden `Smith VI` while `V` and `III` worked). #656's sweep found the same class from the other side: C12 takes the word ending the name before a comma as EVIDENCE, and its first draft asked the suffix-word tag, so `John Smith VI, AD` missed the rule `III` reached, and then counted the whole part, so `Smith VI, AD` fired with `VI` as its own name word and `Smith V, AD` with a `V` that S10 reads as the family. A rule that weighs a numeral as a generation must ask the predicate that READS it as one (the trailing fork, `_vocab.reads_as_generation`, and its words-to-spare count), not the vocabulary that lists some of its spellings. The same split runs through the credentials (`MA` listed, `X.Y.Z.` by the dotted shape, `XYZ` by the caps shape, `MD/PhD` by its listed chunks and `PU-PH` by the caps shape's joined spelling, #752, each with its joiner glued, spaced and glued to one side, which are three token streams -- and a new chunk reading inherits the old one's exceptions, which #752's first draft did not (`Jack I-V` lost its family to the listed numerals, and the fix's length test counted a chunk's period, so `Jack I.-V.` did too, found by the second review), and `Ph. D.` merged by group into one flagged piece the peel's walk never holds -- #603 found #602's run-start predicate refusing it too, so `Smith, John Ph. D. Jones` kept middle 'Jones' where `Smith, John PhD Jones` read suffix 'PhD Jones') and the titles (`Prof.` period-marked and read at the end of a name, `Prof` bare and a name word there, and `Mr. and Mrs.` a run P3 joins, carrying a piece tag a one-word vocabulary test never sees -- PR #735's review found C12's hand copy of C13's title test (C8's until its 2026-10-19 split) reading `John Smith Jr., Mr. and Mrs. AD` as all suffix; ask C13's predicates in C13's order, `not is_suffix_piece` then `is_title_piece`, not the token tags, since `is_title_piece` alone reads the duals `Ms` and `Sr.` as titles). When a change touches a word class, test one spelling from each path. The same holds for a separator's SPACING: a comma (or any punctuation) can stand alone, glue to the word before it or glue to the word after, and each is a different token stream. #653's first fix swept `(née, Jones)` and missed `(née , Jones)`, where the kept comma became a word after the marker and M3 read the clause as maiden ', Jones'; its review found it. Put all three spacings in any grid around a separator. And a separator glued to a word hides the word from every vocabulary test, not only the one being changed: the semicolon delimiter's first draft read a glued word's stem in C1's run test alone and put 365 of ClinicalTrials.gov's reversed names right, missing the ones C13, C4 and C6 read off a word's TAGS (`MBBS;`, `MA;`); tagging the stem put 412 right, and reading it again in the comma decision's shape tests, which read a word's text rather than its tags (`MBBS;`), 431 (decisions/C10, 2026-10-24). A rule over a glued spelling reads the stem everywhere the word is read, its tags and every test of its text, and is swept against the space twin -- with a declared word delimiter too, which a substring test finds inside other words ('Stand' under ' and ', the review of the same change). A full stop INSIDE a word has four spellings of its own: initials run together (`J.R.R.`), joined words each closed by a stop (`Lt.Gov.`, `Msc.Ed.`), a joined word or name closed by none (`St.John`, `J.Smith`), and an abbreviation's own ending, closed or not (Italian `Sig.ra`, Latvian `Dr.philol.`). Shape tells the last from none of the others, so a rule over the shape decides which it costs; #838 found the ending only through the snapshot's CLDR row `Dut.ra Prof.ra`, and its first narrowing traded `St.John` for `Sig.ra` while the snapshot listed the `Dr.philol.` move among moves read as intended, until the design-docs review named it. Initials run together are also how some SURNAMES are written (Nepali `B.K.`, `K.C.`), and a caseless script writes its abbreviations, degrees included, as dotted letters (Thai `พ.บ.`): #864 found S3's position reading taking the first from 2.4.0 on, visible only once #866's Nepal candidates gave the corpora a population that writes them, and its first fix moved CLDR's Thai degrees into the name. A rule over dotted letters reads both before it ships. Capitals between two name words have the same second reading: some writers put their own initials there (`Maria Ana MA Silva`), so a rule that reads a word's capitals as a credential reads the initials too; #785 met it only in ctgov's field moves, no grid having held the shape, and accepted it (rules/S8). A predicate over ADJACENT PAIRS of words meets two shapes no grid of one word holds: an overlapping pair and a joining word beside an initial. #752's split-credential test (phase 2e) read `Dr. Ph.` in `Jane Doe Dr. Ph. D.` as the listed `drph`, leaving `D.` a word of its own (found by the maiden take's candidate grid), and a particle beside a surname written short as the listed `ded` (`DE D JUNIOR`, found only in tse-donors' readings), so put a title, a particle and a connective beside each pair spelling, and a pair overlapping another, in the grid. +**Then sweep the spellings: a class reached by vocabulary is also reached by SHAPE.** A test that uses only the listed spelling of a word class walks only the vocabulary path, and the shape path is a separate branch that can be missing while every test passes. Roman numerals are the sharpest case: `ii`, `iii` and `iv` are suffix vocabulary; `i` and `v` are vocabulary too but initial-shaped, so `is_suffix_piece` vetoes them and only the numeral fork reads them; and `vi` through `x` are in no list at all, read by the fork's shape test alone (checked 2026-10-04 against `Lexicon.default()`) -- which stops at `x`: `_vocab._ROMAN` matches I to X, so `John Smith XI` reads `XI` as a name word (re-checked 2026-10-07, #614's /simplify, which found a docstring claiming "any length"). And the fork counts a ONE-letter numeral besides, since #718 (rules/S10): it is the suffix only where the name has a word to spare, so `John V` and `John Smith V` take different branches of one fork, a one-case name being counted like capitals whatever the letter -- since #721 a one-case one-letter numeral carries the `initial` tag its capital does, and since #723 every one-case bare letter does (rules/T4), and a bare letter in front of the numeral is initial-shaped as its capital is, so `john v` reads as `JOHN V` does over `test_a_one_case_name_reads_as_its_capitals`'s grid (comma shapes, orders, a maiden clause, a particle chain, a bare letter in front, a trailing title, a non-ASCII letter in front: 315 of its then 1,872 pairs disagreed at 2bfa9533, before #721, and none after). Its first version claimed "every shape" with no bare letter in front and no trailing title in the grid, and #721's reviews found both broken; a grid's claim is the grid -- put a head-plus-numeral row beside every words-to-spare row. So a change that handles `V` and `III` can still miss `VI` — #610 found exactly that, the maiden take's candidate loop admitting a shape-only numeral in one position where the peel reads it in two (`Jane Doe nee Smith VI Prof.` kept maiden `Smith VI` while `V` and `III` worked). #656's sweep found the same class from the other side: C12 takes the word ending the name before a comma as EVIDENCE, and its first draft asked the suffix-word tag, so `John Smith VI, AD` missed the rule `III` reached, and then counted the whole part, so `Smith VI, AD` fired with `VI` as its own name word and `Smith V, AD` with a `V` that S10 reads as the family. A rule that weighs a numeral as a generation must ask the predicate that READS it as one (the trailing fork, `_vocab.reads_as_generation`, and its words-to-spare count), not the vocabulary that lists some of its spellings. The same split runs through the credentials (`MA` listed, `X.Y.Z.` by the dotted shape, `XYZ` by the caps shape, `MD/PhD` by its listed chunks and `PU-PH` by the caps shape's joined spelling, #752, each with its joiner glued, spaced and glued to one side, which are three token streams -- and a new chunk reading inherits the old one's exceptions, which #752's first draft did not (`Jack I-V` lost its family to the listed numerals, and the fix's length test counted a chunk's period, so `Jack I.-V.` did too, found by the second review), and `Ph. D.` merged by group into one flagged piece the peel's walk never holds -- #603 found #602's run-start predicate refusing it too, so `Smith, John Ph. D. Jones` kept middle 'Jones' where `Smith, John PhD Jones` read suffix 'PhD Jones') and the titles (`Prof.` period-marked and read at the end of a name, `Prof` bare and a name word there, and `Mr. and Mrs.` a run P3 joins, carrying a piece tag a one-word vocabulary test never sees -- PR #735's review found C12's hand copy of C13's title test (C8's until its 2026-10-19 split) reading `John Smith Jr., Mr. and Mrs. AD` as all suffix; ask C13's predicates in C13's order, `not is_suffix_piece` then `is_title_piece`, not the token tags, since `is_title_piece` alone reads the duals `Ms` and `Sr.` as titles). When a change touches a word class, test one spelling from each path. The same holds for a separator's SPACING: a comma (or any punctuation) can stand alone, glue to the word before it or glue to the word after, and each is a different token stream. #653's first fix swept `(née, Jones)` and missed `(née , Jones)`, where the kept comma became a word after the marker and M3 read the clause as maiden ', Jones'; its review found it. Put all three spacings in any grid around a separator. And a separator glued to a word hides the word from every vocabulary test, not only the one being changed: the semicolon delimiter's first draft read a glued word's stem in C1's run test alone and put 365 of ClinicalTrials.gov's reversed names right, missing the ones C13, C4 and C6 read off a word's TAGS (`MBBS;`, `MA;`); tagging the stem put 412 right, and reading it again in the comma decision's shape tests, which read a word's text rather than its tags (`MBBS;`), 431 (decisions/C10, 2026-10-24). A rule over a glued spelling reads the stem everywhere the word is read, its tags and every test of its text, and is swept against the space twin -- with a declared word delimiter too, which a substring test finds inside other words ('Stand' under ' and ', the review of the same change). A full stop INSIDE a word has four spellings of its own: initials run together (`J.R.R.`), joined words each closed by a stop (`Lt.Gov.`, `Msc.Ed.`), a joined word or name closed by none (`St.John`, `J.Smith`), and an abbreviation's own ending, closed or not (Italian `Sig.ra`, Latvian `Dr.philol.`). Shape tells the last from none of the others, so a rule over the shape decides which it costs; #838 found the ending only through the snapshot's CLDR row `Dut.ra Prof.ra`, and its first narrowing traded `St.John` for `Sig.ra` while the snapshot listed the `Dr.philol.` move among moves read as intended, until the design-docs review named it. Initials run together are also how some SURNAMES are written (Nepali `B.K.`, `K.C.`), and a caseless script writes its abbreviations, degrees included, as dotted letters (Thai `พ.บ.`): #864 found S3's position reading taking the first from 2.4.0 on, visible only once #866's Nepal candidates gave the corpora a population that writes them, and its first fix moved CLDR's Thai degrees into the name. A rule over dotted letters reads both before it ships. Capitals between two name words have the same second reading: some writers put their own initials there (`Maria Ana MA Silva`), so a rule that reads a word's capitals as a credential reads the initials too; #785 met it only in ctgov's field moves, no grid having held the shape, and Derek chose the initials (rules/S8: a name word behind capitals keeps them a name word where no family comma stands). A predicate over ADJACENT PAIRS of words meets two shapes no grid of one word holds: an overlapping pair and a joining word beside an initial. #752's split-credential test (phase 2e) read `Dr. Ph.` in `Jane Doe Dr. Ph. D.` as the listed `drph`, leaving `D.` a word of its own (found by the maiden take's candidate grid), and a particle beside a surname written short as the listed `ded` (`DE D JUNIOR`, found only in tse-donors' readings), so put a title, a particle and a connective beside each pair spelling, and a pair overlapping another, in the grid. **And the encoding: a letter class written `[^\W\d_]` or tested with `isalpha()` is blind to a combining mark.** Python's `\w` and `isalpha()` do not match Mn, so a letter written as base plus accent (NFD) ends a word early; the vocabulary path is immune because `_normalize` NFC-folds, the shape path is not. #652: `Dr. inž. Janez Novak` read `inž.` as the given name when the ž was decomposed, and `Janez Novak, ŽŠČ` reversed, in two families that share the blind spot — the letter-shape regexes (`period_abbreviation`, `_DOTTED`, `_CHUNKED`, `_PAIRED_INITIALS`), now spelled with `_vocab._LETTER` (a letter plus the general combining blocks; `test_regex_sync` compares the two `_MARKS` strings and pins `period_abbreviation` against config's copy), and the caps predicates, which NFC-compose a non-ASCII word before `isalpha()` (`is_caps_word`, behind C-level `isalpha()`/`is_normalized` tests, so already-composed text pays no frame — measured 2026-10-12, `Smith, José` was 193 frames before and after; the first version called it for every non-ASCII word and cost one frame each, and no guard saw it because the reference name is ASCII). A width test must count letters, not code points (`composed_len`): admitting a decomposed word to the caps set put `John Smith, MÁ` on the pair exception's wrong side, and the review's grid found it where the rows (all three letters long) did not — put a two-letter word in every caps grid. The initial shape (`_INITIAL`, `\w\.`, v1-verbatim and public) is not widened but asked of the COMPOSED word, in `is_initial_shaped` and in `_render`'s two copies; and the single-letter connective tests count composed letters, since a decomposed `й` is two code points (#665, where `É. y É. Smith` and `John й Smith` read differently decomposed). Compose the WHOLE predicate, not one half: `is_initial` is a shape ANDed with the #320 script veto, and composing the shape alone made a decomposed `씨.` an initial, since decomposed hangul is conjoining jamo outside every script range (#665's first version; `in_initialless_script` composes too now). Not covered, deliberately: the marks only some scripts write (Indic vowel signs, Arabic and Hebrew points), declined in decisions/nfd-shapes because widening the class would move readings nobody had measured; rules/H2's Accepted clause records the one consequence it names, that a period-marked abbreviation in Bengali or Devanagari never reads as a title by shape. The guard is `test_a_decomposed_name_renders_as_its_composed_twin` over the CLDR corpus plus `test_a_decomposed_abbreviation_reads_as_its_composed_twin` (rows × three orders × EVERYWHERE); the corpus guard reaches a shape only if some corpus name does, which is how the caps shape was missed until review, so when adding a shape rule add a row there. Four of #652's rows and two of #665's pass with every fix reverted (measured) and are parity pins, and the test compares ambiguity kinds and `initials()` too, since #665's `John й Smith` differed in its report; `_DOTTED` has no observable input and is pinned by a direct call. diff --git a/docs/customize.rst b/docs/customize.rst index 416491d29..dde5d2561 100644 --- a/docs/customize.rst +++ b/docs/customize.rst @@ -337,8 +337,8 @@ The full reading of a bare marked acronym, in order: - **ALL CAPITALS** in a mixed-case name leans to the credential: it is a suffix even with no words to spare (``"Jack MA"``). The capitals settle it as the periods do, so no ambiguity is reported, and a word - after it reads as part of the suffix (``"John Smith MA Jones"`` gives - suffix ``MA Jones``). + after it in the given part after a family comma reads as part of + the suffix (``"Smith, John MA Jones"`` gives suffix ``MA Jones``). - **Title-case** in a mixed-case name leans to the surname: it stays the family name even WITH words to spare (``"John Smith Ma"``). - **No signal** — all lowercase in a mixed-case name, or any spelling diff --git a/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md b/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md index 8f42b3cb1..1d63b4e9e 100644 --- a/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md +++ b/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md @@ -10,7 +10,7 @@ - `Freiherr von Berg MA` reads family 'von Berg', suffix 'MA', the lean ending P2's chain with no words to spare — 2026-09-15 (Derek). - The whole-PR review's five readings stand: an acronym the particle chain absorbs reports `suffix-or-name` (`John van der Berg Ma`); the CJK stage asks the post-comma run with the one-case fact; a by-shape credential in a tail segment raises no `comma-structure`; a word a caller lists as a surname is never caps-shaped; and a dotted listed member takes the lean — 2026-09-18, WHOLE-PR REVIEW ROUND. -- A listed member of `suffix_acronyms_ambiguous` written in capitals in a name of mixed case is settled as the credential by its writing and reports nothing at any slot that reads it (`John Smith MA`, `Jack MA` but its `given-or-family`, `John Smith, MA`, `Doe, John DO`), and it starts the credential's trailing run (rules/S8: `John Smith MA Jones` → suffix 'MA Jones'); the clash settled is the one with the name reading alone, so the word keeps its tags and its other readings (`Doe, John DO` keeps family 'Doe'), a maiden clause still keeps it as its first word and reports it (`Jane Doe nee MA`), and nothing before a comma leaves it the given name, reported (`, MA Smith`) — 2026-10-25 #785 (Derek, 2026-10-23); 2026-10-25 #785, review. +- A listed member of `suffix_acronyms_ambiguous` written in capitals in a name of mixed case is settled as the credential by its writing and reports nothing at any slot that reads it (`John Smith MA`, `Jack MA` but its `given-or-family`, `John Smith, MA`, `Doe, John DO`), and it starts the credential's trailing run (rules/S8) after a family comma (`Smith, John MA Jones` → suffix 'MA Jones') and elsewhere only where no name word stands behind it, a name word there keeping the capitals a name word as the writer's initials may be (`John Smith MA Jones`, `Maria Ana MA Silva` keep their family names; 2026-10-26 (Derek)); the clash settled is the one with the name reading alone, so the word keeps its tags and its other readings (`Doe, John DO` keeps family 'Doe'), a maiden clause still keeps it as its first word and reports it (`Jane Doe nee MA`), and nothing before a comma leaves it the given name, reported (`, MA Smith`) — 2026-10-25 #785 (Derek, 2026-10-23); 2026-10-25 #785, review. Elsewhere: the case a generation word that is also a name reads (rules/S6's bullet on it) is decisions/S11's 2026-10-25 #752 entry. @@ -37,9 +37,11 @@ Open: none in this file. - 2026-10-25 #785 (Derek, 2026-10-23) — CAPITALS SETTLE A LISTED CREDENTIAL, AND A CLASH SETTLED BY THE WRITING IS NOT REPORTED (#752's phase 2g: the form step of decisions/vocabulary-model's 2026-10-23 #752 entry, item 7, answering #785's first three questions; its fourth was answered there, no new kind). In a name written in more than one case a listed member of the ambiguous class written in capitals was already READ as the credential (the 2026-09-14 entries above); it was still REPORTED as a fork at every slot, while the same word written with its periods (`M.A.`, rules/S3) reported nothing, and it did not start S8's run where the dotted spelling did. #785 asked whether the two writings should answer alike: they do now. `John Smith MA` reports nothing where it reported `suffix-or-name`; `John Smith MA Jones` reads suffix 'MA Jones', reported for 'Jones', where it read middle 'Smith MA', family 'Jones'; a name written in one case keeps its report (`JOHN SMITH MA`), and so does a Title-case member, which leans to the name and is weighed by the count. HOW, AND WHY NOT A RETAG. The phase 2 spike settled the word by retagging it as an unambiguous credential, and that erased its OTHER readings: `do` is a particle too, and once it stopped being a member of the ambiguous class rules/P6's attachment took it (`Doe, John DO` → family 'DO Doe'), and the maiden clause, whose first word is kept by the clause whatever it is, lost its maiden name (`Jane Doe nee MA`). So the word keeps every tag, and one predicate (`_pieces.settled_by_case`, a listed acronym whose lean is the credential, a suffix word that is also a name being rules/S11's) is asked where a reading is chosen: the trailing peel takes a settled member without recording a pick, so no reporter downstream meets it; the comma's decision and the given part's trailing slot leave its report out; and S8's run start admits it, the particle test in front still refusing `DO` (`John Smith DO Jones` keeps family 'DO Jones'). The case asked is ParseState's one-case fact over the name's own words, so a word in a maiden clause or in brackets is read by the whole name's case, never its own; the spike asked a bracketed word's own case and moved 7 FEC reports for nothing. WHERE IT STILL REPORTS. Where something before the writing keeps the word a name it is read as before and reported: the first word a maiden clause takes (rules/M2, `Jane Doe nee MA` → maiden 'MA'), and the word after a comma with nothing in front of it, which C4's count leaves the given name (`, MA Smith` → given 'MA'). A bracketed member is rules/S1's, unchanged here (phase 2l). - ACCEPTED. A writer's initials in capitals between the given names and the family name, where they spell a listed member, start the run, and the family name reads as a suffix (rules/S8's Accepted clause, `Maria Ana MA Silva` → suffix 'MA Silva', as `Maria Ana M.A. Silva` already read). In ctgov-officials (fetched 2026-10-21), 9 distinct names write a listed member in capitals between two Title-case name words with no comma, and 6 of them now read this way. The other 5 field moves there are lists of credentials after a comma that the parse reads as a reversed name, a credential taken for the given name (unchanged), whose list behind the member now reads as the suffix where it read as middle names. No name in that shape is written with the periods there. + ACCEPTED (superseded by the 2026-10-26 (Derek) entry below, which reads such initials as names). A writer's initials in capitals between the given names and the family name, where they spell a listed member, start the run, and the family name reads as a suffix (rules/S8's Accepted clause, `Maria Ana MA Silva` → suffix 'MA Silva', as `Maria Ana M.A. Silva` already read). In ctgov-officials (fetched 2026-10-21), 9 distinct names write a listed member in capitals between two Title-case name words with no comma, and 6 of them now read this way. The other 5 field moves there are lists of credentials after a comma that the parse reads as a reversed name, a credential taken for the given name (unchanged), whose list behind the member now reads as the suffix where it read as middle names. No name in that shape is written with the periods there. MEASURED (2026-10-25, Python 3.11.17, against master a46b3a6e, every local check with `--base` that checkout): the committed snapshot moves 54 names, 48 a dropped report and 6 a field, every one a case-row or rules text, and gains 20 texts and loses 1, the rows added and those rewritten to keep reaching their emitter; every CLDR, GND, Wikidata and locale agreement record matches. ctgov-officials moves 1,778 readings: 1,767 drop a report and nothing else, each a capitals listed member (`DO` 1,090, `MA` 426, `BA` 184, `JD` 35, `PA` 11, the rest five or fewer; by shape `Aa Aa, DO` 607, `Aa Aa Aa, DO` 244, `Aa Aa, MA` 176), and 11 move a field, each the run a settled member starts (the 6 accepted above and the 5 credential lists), 10 of them gaining the report on the name word the run absorbs; no other reading gains a report. fec-individuals, written in one case, and tse-donors match the base; psc-individuals matches it on every record (1,004,785 of 1,039,171 agreeing). The phase 2 spike's figures (71 committed, 1,853 ctgov, 7 FEC) were taken on another base with the retag, and are not this change's. FRAMES (`sys.setprofile` call events over a second parse, py3.11): the reference row and plain names cost what they did (`Dr. Juan0001 de la Vega III` 305, `John Smith` 149, `Smith, John` 158, `John Smith, PhD` 202); a name holding a member pays for the predicate where it is asked (`John Smith Ma` 213 → 220, `JOHN SMITH MA` 212 → 215, `John Smith MA` 212 → 203, the pick and its report gone) and for the run it now reads (`John Smith MA Jones` 232 → 259). Recompute: `uv run python tools/differential/snapshot.py --since `; for each local corpus `snapshot.py --local --base `, and `local_agreement.py psc-individuals --base `; the ctgov shape count by reading `corpus.jsonl` of ctgov-officials, keeping each distinct name whose part before its first comma holds, after two words, a word whose letters are a listed member written in capitals between two words that each open with a capital and are not wholly in capitals, and counting those whose suffix holds it, on this tree and on the parent. - 2026-10-25 #785, review — THREE SITES THE FIRST VERSION LEFT ASKING THE OLD QUESTION, found by the PR's code review on a grid of 104,676 parses (125 shapes, 23 spellings of the member as written, in upper and in lower case, three orders, four policies) against master, and a twin grid comparing each capitals member with its dotted spelling. (i) The comma's report on the part's opening word was withheld only where every listed member of the part was settled, so `Smith, MA PhD Ma` still reported its settled `MA` beside the Title-case `Ma` S7's company reads; it asks the word's own writing now. (ii) P2's chain stopped at a suffix word and not at a settled member, so after a family comma with a title-particle head the chain took the member and the words behind it into the given name in silence (`Smith, Freiherr von Berg MA Jones` → given 'von Berg MA Jones', where `Smith, Freiherr von Berg M.A. Jones` reads suffix 'M.A. Jones'); `_pieces.chain_run_end` asks `settled_by_case` now, the chain's units with it, and the member starts the run. (iii) P5's bound given-name join took a settled member as the given name's second word (`Smith, Abdul MA` → given 'Abdul MA' in silence, where `Smith, Abdul M.A.` reads suffix 'M.A.'); it declines one now, as it declines a suffix word (decisions/P5). Measured after the fixes, against the same master: the twin grid's capitals-against-dotted disagreements fall from 4,062 on master to 2,250, every one of which master shares; the main grid's removed reports all name a settled member and no report is added; ctgov-officials, fec-individuals and tse-donors move exactly as the entry above records, the three sites reaching no name in them. The design-docs review found the API reference (`AmbiguityKind.SUFFIX_OR_NAME`'s docstring), rules/P6 and Standing lines in decisions/M2, S4 and S5 still stating the old reports, and S8's Accepted clause narrower than its cost: a family name written in capitals by convention reads the same way (`Jean Pierre MA DUPONT` → suffix 'MA DUPONT'), and a name word behind the member that is title vocabulary reads as a title in silence (`Mary Jane MA King` → title 'King', as `Mary Jane M.A. King` reads); all are amended. Left as it reads: `John Smith MA y` reads family 'MA y' where `John Smith PhD y` reads suffix 'PhD y', a trailing connective no writing produces. - 2026-10-25 #785, review of the fix — THE CHAIN STOP AND THE JOIN'S DECLINE ARE NARROWED TO THE GIVEN PART AFTER A FAMILY COMMA. SUPERSEDES the review entry's (ii) and (iii) where they read "wherever". Applied everywhere, both left a settled member where nothing then took it, read as a name word in silence: the chain stopped in front of a member the trailing read declined (`Freiherr von Berg MA Ma` → given 'von Berg', middle 'MA', family 'Ma', and under the family-first orders given 'MA'; 162 parses of the fix review's grid, the only moves of the fix away from both master and the dotted spelling), and the join declined one with no comma (`Abdul MA Smith` → middle 'MA'). After a family comma the member always has a reader, the given part's trailing slot or the run S8 starts behind the given word, so the two widenings stand there and nowhere else; outside it the readings are master's (decisions/P2, decisions/P5). The comma's opening-word guard was found sound. +- 2026-10-26 (Derek), #785 — A NAME WORD BEHIND THE CAPITALS KEEPS THEM A NAME WORD, WITH NO FAMILY COMMA. SUPERSEDES the 2026-10-25 #785 entry's ACCEPTED paragraph and its review entry's widening of that clause to capitals family names and title words. Derek, on PR #895: narrow #785's second question rather than accept its cost. In a name with no family comma, a listed member settled by its capitals does not start rules/S8's credential run where a name word stands right behind it: a particle, or a word in Title case or in capitals that is no suffix word, no word of the class by shape and no member its own capitals settle. Such a word is evidence the capitals are the writer's initials (`Maria Ana MA Silva`) or part of a name written in capitals by convention (`Jean Pierre MA DUPONT`, the capitals family name counting as the name word, a reading the narrowing decides rather than leaves open), as the design's cue table has Title case lean to the name; and a title word behind them stays the family name it is (`Mary Jane MA King`). Each of the four keeps the reading 1.4.0 through 2.6.0 gave (middle 'Ana MA', family 'Silva', and so on). The cost, Accepted in rules/S8: `John Smith MA Jones` keeps middle 'Smith MA', family 'Jones', in silence as before, where `John Smith M.A. Jones` reads suffix 'M.A. Jones'. A credential behind the member still lets it start the run (`John Smith MA JD Jones` → suffix 'MA JD Jones'), and after a family comma nothing changes: the run starts behind the given word whatever follows (`Smith, John MA Jones` → suffix 'MA Jones'), which is where ctgov's five credential-list fixes stand. `_pieces.run_start` asks `_name_word_behind` of a settled member, and the given part's predictor (`trailing_run_start`) passes `initials_cue=False` so it keeps agreeing with assign's walk there. A first draft keyed the test on the class tag, which the caps switch (`CapsSuffixes.EVERYWHERE`) also writes on a numeral it admits by shape, so `John Smith VI Ma` lost its generation reading under that switch (middle 'Smith VI', family 'Ma'); the test asks `settled_by_case` now, and a case row holds the shape. + MEASURED (2026-10-26, Python 3.11.17, against master a46b3a6e with `--base`): ctgov-officials moves 1,772 readings, the 1,767 dropped reports of the entries above and 5 field moves, the credential lists after a comma; the 6 initials readings are gone and nothing else moves. fec-individuals, tse-donors and psc-individuals match the base, and every agreement record matches. The committed snapshot moves 55 names against master, 48 a dropped report and 7 a field, every one a case-row or rules text. On the first review's grid of 104,676 parses, master against this tree, 1,227 parses move a field, each a mixed-case name whose capitals member now reads as a suffix, every report removed names a member that ends in the suffix field, and none is added; capitals-against-dotted disagreements are 3,078, every one of which master shares. diff --git a/docs/design/decisions/S8-credentials-trailing-run.md b/docs/design/decisions/S8-credentials-trailing-run.md index 61590d88e..ada99a001 100644 --- a/docs/design/decisions/S8-credentials-trailing-run.md +++ b/docs/design/decisions/S8-credentials-trailing-run.md @@ -10,7 +10,7 @@ Examples name each field as the default order reads it; under the family-first o - An honorific or generational word starts the run as a credential does, whatever its script (`राम शर्मा साहब मोहन` → suffix 'साहब मोहन'); the one-character honorifics stay out by the two-letter minimum — 2026-10-14 #679. - A lone particle counts toward the two name words only where the credential stands right behind it (`John von PhD Jones` → family 'von', suffix 'PhD Jones', under the family-first orders given 'von'); a particle run counts once, and a particle the run took is not asked (`Freiherr von vd PhD Jones` keeps middle 'PhD') — 2026-10-10 #627. - Lifting the credential out of the name, the family-comma path's older reading (`John Smith PhD Jones` as family 'Jones', suffix 'PhD'), was declined — 2026-10-04 #602. -- An ambiguous acronym written with its periods starts the run (`John Smith M.A. Jones` → suffix 'M.A. Jones'); and so, since 2026-10-25 #785, does a listed member written in capitals in a name of mixed case, its writing settling it (`John Smith MA Jones` → suffix 'MA Jones'; `John Smith Ma Jones` and `JOHN SMITH MA JONES` keep family 'Jones', and `John Smith DO Jones` family 'DO Jones', its particle reading refusing the start) — 2026-10-20 #761; 2026-10-25 #785. +- An ambiguous acronym written with its periods starts the run (`John Smith M.A. Jones` → suffix 'M.A. Jones'); and so, since 2026-10-25 #785, does a listed member written in capitals in a name of mixed case, its writing settling it, after the given word after a family comma (`Smith, John MA Jones` → suffix 'MA Jones') and elsewhere only where no name word stands behind it (`John Smith MA JD Jones` → suffix 'MA JD Jones', while `John Smith MA Jones` keeps family 'Jones', 2026-10-26 (Derek)); `John Smith Ma Jones` and `JOHN SMITH MA JONES` keep family 'Jones', and `John Smith DO Jones` family 'DO Jones', its particle reading refusing the start — 2026-10-20 #761; 2026-10-25 #785; 2026-10-26 (Derek), #785. Not standing: the split of 2026-10-17 #682, carried out. @@ -34,3 +34,4 @@ Open: none in this file. - 2026-10-20 (Derek), #761 — AN AMBIGUOUS ACRONYM WRITTEN WITH ITS PERIODS STARTS THE RUN. The skeleton migration (PR #783) found S8 ("a member of the ambiguous class" never starts the run) and S3 (an ambiguous acronym written with its periods "counts unambiguously") both claiming `John Smith M.A. Jones`, with no text naming the winner; the parser reads suffix `M.A. Jones`. Derek: "writing periods makes it unambiguously not a name". S3 wins, written as a Precedence bullet in rules/S8; no behavior moves. The bullet is scoped to the periods: the capitals alone do not start the run (`John Smith MA Jones` reads middle `Smith MA`, family `Jones`), a question tracked in [#785](https://github.com/derek73/python-nameparser/issues/785). - 2026-10-25 #784 — A NUMERAL THAT READS AS THE GENERATION STARTS THE RUN, WHATEVER ITS SPELLING. The run start asked the suffix vocabulary, so a listed numeral started it and a numeral S10 reads by its shape did not: `John Smith III Jones` → suffix 'III Jones', `John Smith VI Jones` → family 'Jones'. It now asks the numeral's predicate (decisions/vocabulary-model's 2026-10-25 2f entry) for a numeral of two or more letters, with the word in front and the name's case, so a Title-case `Vi` in a mixed-case name, which leans to the name, and a numeral behind an initial start nothing. A single letter still never starts the run. The numeral starting the run reports `suffix-or-name` where its shape alone reads it, as S10's numeral does anywhere it is weighed. The given part after a family comma starts its run the same way (`Smith, John VI Jones`). Separately, #819's reading of a lone letter ahead of a further comma stops the run short of that letter (decisions/C9's 2026-10-25 #819 entry). - 2026-10-25 #785 (Derek, 2026-10-23) — A LISTED MEMBER ITS CAPITALS SETTLE STARTS THE RUN, as its dotted spelling has since 2026-10-20 #761, answering #785's second question. Asked of the run start's one predicate (`_pieces.starts_a_credential_run`), whose particle test still refuses `DO`, so a member's other readings answer as before; the inline pre-tests ahead of it (the trailing run's start and the given part's) admit a member, as they admit a numeral. Accepted: a writer's initials in capitals between the given names and the family name that spell a listed member start the run too (rules/S8's Accepted clause). The record, with what was measured, is decisions/S6's entry of this date. +- 2026-10-26 (Derek), #785 — WITH NO FAMILY COMMA A NAME WORD BEHIND A SETTLED MEMBER KEEPS IT FROM STARTING THE RUN. SUPERSEDES the rule's Accepted clause of 2026-10-25 (capitals initials and capitals family names reading as suffixes): `Maria Ana MA Silva` keeps family 'Silva', and the Accepted cost is now `John Smith MA Jones` keeping family 'Jones'. The record is decisions/S6's entry of this date. diff --git a/docs/design/decisions/vocabulary-model.md b/docs/design/decisions/vocabulary-model.md index 75c97a862..38f3dfc38 100644 --- a/docs/design/decisions/vocabulary-model.md +++ b/docs/design/decisions/vocabulary-model.md @@ -17,7 +17,7 @@ The vocabulary was a set of base sets plus marker subsets (`particles` and `part - Phase 2 is to put vocabulary, declared name and shape readings in one pool, derive `may_be_given` and `may_be_name` from declared name readings (a count-free one carrying a `note`, `ld` left ungated, `may_be_initial` kept), and decide every clash by one gate of four steps, floor, form, structure and count, asked at the sites that decide today; the reading not taken is to surface as `Ambiguity.alternative` in 2.x and the reading taken as `Token.reading` in 3.0 — 2026-10-23 #752. - Every numeral rule asks one predicate, `_vocab.reads_as_generation`, the vocabulary's own numerals read inside it; a numeral of two or more letters is the generation wherever the trailing run holds it, while a one-letter one before a credential stays the name word decisions/S10's #721 entry made it — 2026-10-25 #752, phase 2f. - A credential written split across a space is one word, a member of the ambiguous class that the count decides, and the Ph./D. pair starts S8's run from where it stands (decisions/S12) — 2026-10-25 #752, phase 2e. -- A listed credential that is also a name, written in capitals in a name of mixed case, is settled as the credential by its writing: it reports nothing and starts the credential's run, and keeps its other readings, the clash settled at the sites that choose a reading rather than by retagging the word (decisions/S6) — 2026-10-25 #752, phase 2g, #785. +- A listed credential that is also a name, written in capitals in a name of mixed case, is settled as the credential by its writing: it reports nothing and starts the credential's run where no name word stands behind it with no family comma (2026-10-26, Derek), and keeps its other readings, the clash settled at the sites that choose a reading rather than by retagging the word (decisions/S6) — 2026-10-25 #752, phase 2g, #785. - Phase 2's title batches go in order of lone-head breakage in PSC, `buddha`, `siddha` and `gyani` in the default gate and `guru` left to a `ne` pack — 2026-10-24 #752. - `Dean Smith` reads given `Dean`. The contested-head gate is to cover the words outside `titles` (`dean`, `justice`, `major`), and a title that is also a given name gets a declared given reading one word at a time, in measured batches — 2026-10-15/16; 2026-10-23 #752. - Order and shape are separate: a language supplies only a default order, and the shapes (where titles, suffixes, particles and commas sit) belong to each order and live in the core; vocabulary readings are to settle a clash and name readings only to keep the gate on — 2026-10-15/16. @@ -208,3 +208,4 @@ Open: #752 (phase 2, derived ambiguity; its sub-phases 2a to 2m, each its own PR Adding the two fields changes `Lexicon`'s pickle layout, as 2.6 did. - **2026-10-25 #752, phase 2g — CAPITALS SETTLE A LISTED CREDENTIAL**, carrying out item 7 of the 2026-10-23 #752 entry above (#785's first three questions). In a name written in more than one case a listed member of the ambiguous class written in capitals reports nothing where it read as the credential (`John Smith MA`, `John Smith, MA`, `Doe, John DO`), and it starts the credential's run as its dotted spelling does (`John Smith MA Jones` → suffix 'MA Jones'). The word keeps its tags, so the clash settled is the one with the name reading alone: `Doe, John DO` keeps family 'Doe', the particle reading still refusing the run start, and `Jane Doe nee MA` keeps its maiden name and its report. A name in one case keeps its report (`JOHN SMITH MA`). The record, with what was measured and the accepted cost, is decisions/S6's entry of this date (rules/S6, S8). +- **2026-10-26 (Derek), #785 — PHASE 2g NARROWED: a capitals member with a name word behind it, and no family comma, does not start the credential's run**, the name word being evidence of initials; `Maria Ana MA Silva`, `Jean Pierre MA DUPONT` and `Mary Jane MA King` keep the readings 1.4.0 through 2.6.0 gave, and `John Smith MA Jones` keeping family 'Jones' is the cost. The record is decisions/S6's entry of this date. diff --git a/docs/design/rules/S6-case-signal-at-the-suffix-slot.md b/docs/design/rules/S6-case-signal-at-the-suffix-slot.md index 0d72d5930..1e73dc24b 100644 --- a/docs/design/rules/S6-case-signal-at-the-suffix-slot.md +++ b/docs/design/rules/S6-case-signal-at-the-suffix-slot.md @@ -4,7 +4,7 @@ **Then:** written case is evidence beside the count (S4), and it speaks only in a name written in more than one case: there a member of the ambiguous set written in capitals reads as the credential even with no words to spare, and one written in any other cased form that is not wholly lower reads as the name even with words to spare. -- **A clash settled by the writing is not reported**: a listed member its capitals read as the credential is no longer a word that might be either, as one written with its periods is not (S3), so no `suffix-or-name` report names it at any slot S4, S5 or C4 reads it. Only the clash with the name reading is settled: the word keeps its other readings, so a particle reading (`DO`) answers P6 as before, and a member's capitals start the credential's trailing run as a credential's do (S8). +- **A clash settled by the writing is not reported**: a listed member its capitals read as the credential is no longer a word that might be either, as one written with its periods is not (S3), so no `suffix-or-name` report names it at any slot S4, S5 or C4 reads it. Only the clash with the name reading is settled: the word keeps its other readings, so a particle reading (`DO`) answers P6 as before, and a member's capitals start the credential's trailing run as a credential's do, where S8 says they do (S8). ``` "John Smith MA" → ambiguities=() "Jack MA" → ambiguities=("given-or-family",) diff --git a/docs/design/rules/S8-credentials-trailing-run.md b/docs/design/rules/S8-credentials-trailing-run.md index 652383014..33639e24a 100644 --- a/docs/design/rules/S8-credentials-trailing-run.md +++ b/docs/design/rules/S8-credentials-trailing-run.md @@ -4,11 +4,14 @@ **Then:** a credential after the name core starts a run to the end of its part (#602): every later word of that part reads as a suffix, except a title word, which reads as a title. -- **Where the run starts.** An unambiguous suffix word of two or more letters, in any script, standing after two name words where no comma divides the name or before a suffix comma, or after the given word in the part after a family comma, makes every later word of that part a suffix. So does a roman numeral of two or more letters that reads as the generation (S10), whether the vocabulary lists it or its shape alone reads it, and so does a listed member of the ambiguous class its writing settles as the credential, written in capitals in a name of mixed case (S6). +- **Where the run starts.** An unambiguous suffix word of two or more letters, in any script, standing after two name words where no comma divides the name or before a suffix comma, or after the given word in the part after a family comma, makes every later word of that part a suffix. So does a roman numeral of two or more letters that reads as the generation (S10), whether the vocabulary lists it or its shape alone reads it, and so does a listed member of the ambiguous class its writing settles as the credential, written in capitals in a name of mixed case (S6) — after the given word in the part after a family comma wherever it stands, and elsewhere not where a name word stands behind it: a particle, or a word in Title case or in capitals that is no credential, which makes the capitals as likely the writer's own initials (`Maria Ana MA Silva`), or a family name written in capitals, as a credential. ``` "John Smith VI Jones" → suffix="VI Jones" - "John Smith MA Jones" → suffix="MA Jones" "Smith, John MA Jones" → suffix="MA Jones" + "John Smith MA JD Jones" → suffix="MA JD Jones" + "Maria Ana MA Silva" → family="Silva" + "Jean Pierre MA DUPONT" → family="DUPONT" + "Mary Jane MA King" → family="King" "Smith, John VI Jones" → suffix="VI Jones" "Smith, John PhD Jones" → suffix="PhD Jones" "John Smith PhD Prof. Ma" → suffix="PhD Ma" @@ -64,12 +67,11 @@ **Accepted:** -- A word of capitals between two name words that spells a listed member of the ambiguous class starts the run as that credential does, whether it is the writer's own initials or a family name written in capitals by the convention that writes it so, and the name word behind it reads as a suffix, reported, or as a title where it is title vocabulary, in silence: the capitals are the credential's writing and these alike, and the periods' spelling already read this way. +- With no family comma, a credential written in capitals with a name word behind it is read as a name word, where the same credential written with its periods starts the run: a name with its credential between its given names and its family name keeps the family name behind the credential, the capitals being read as initials as likely as a credential. ``` - "Maria Ana MA Silva" → suffix="MA Silva" - "Maria Ana M.A. Silva" → suffix="M.A. Silva" - "Jean Pierre MA DUPONT" → suffix="MA DUPONT" - "Mary Jane MA King" → title="King" + "John Smith MA Jones" → family="Jones" + "John Smith MA Jones" → middle="Smith MA" + "John Smith M.A. Jones" → suffix="M.A. Jones" ``` --- diff --git a/docs/release_log.rst b/docs/release_log.rst index 8fe6b2f4b..366b269f0 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -12,7 +12,7 @@ Release Log - **Read a credential written with a space inside it, such as Pharm D, as one credential.** ``John Smith, Pharm D`` gives first ``John``, last ``Smith``, suffix ``Pharm D``, where 1.4.0 through 2.6.0 gave first ``Pharm``, middle ``D``, last ``John Smith``. It counts only where the name has words to spare, so ``Smith, Pharm D`` still gives first ``Pharm``, and one opening with a single letter is still two words (``John Smith, M. D.`` still gives first ``M.``). :issue:`752` - **Fix a split Ph. D. in the middle of a name stopping short of the words after it.** ``John Smith Ph. D. Jones`` gives suffix ``Ph. D. Jones``, as ``John Smith PhD Jones`` does, where 1.4.0 through 2.6.0 gave last ``Jones``, suffix ``Ph. D.``; ``John Smith V Ph. D.`` gives last ``V``, as ``John Smith V PhD`` does, where 1.4.0 through 2.6.0 read ``V`` as a suffix. :issue:`827` - **Read the Brazilian agnomens and French fils as suffixes where the name has a word to spare.** ``Pedro Alves Filho`` and ``Alexandre Dumas fils`` give last ``Alves`` and ``Dumas``, suffix ``Filho`` and ``fils``, where 1.4.0 through 2.6.0 gave middle ``Alves`` and ``Dumas``, last ``Filho`` and ``fils``; ``Neto``, ``Netto``, ``Sobrinho`` and the feminines ``Filha``, ``Neta``, ``Sobrinha`` read the same way. Each is a surname too, so a name of two words keeps it as the last name (``Ana Neto``, ``PEDRO FILHO``) and ``Fils`` in Title case in a mixed-case name stays the last name (``Marie Fils``), the parse reports the choice as a ``suffix-or-name`` ambiguity. ``Lexicon.suffix_words_ambiguous`` lists the words and ``Lexicon.agnomens`` the ones read by the word count alone. :issue:`843`, :issue:`752` - - **Read a credential that is also a name, written in capitals in a mixed-case name, as settled.** ``John Smith MA`` reports no ``suffix-or-name`` ambiguity, where 2.0.0 through 2.6.0 reported one, and ``John Smith MA Jones`` gives suffix ``MA Jones``, where 1.4.0 through 2.6.0 gave middle ``Smith MA``, last ``Jones``, as ``John Smith M.A. Jones`` already read. A name written in one case still reports (``JOHN SMITH MA``), and initials written in capitals before the last name read the same way (``Maria Ana MA Silva`` gives suffix ``MA Silva``). :issue:`785`, :issue:`752` + - **Read a credential that is also a name, written in capitals in a mixed-case name, as settled.** ``John Smith MA`` reports no ``suffix-or-name`` ambiguity, where 2.0.0 through 2.6.0 reported one, and ``Smith, John MA Jones`` gives suffix ``MA Jones``, where 2.0.0 through 2.6.0 gave middle ``MA Jones`` and 1.4.0 middle ``Jones``, suffix ``MA``. A name written in one case still reports (``JOHN SMITH MA``), and with no comma a name word behind the capitals keeps them a name word, since they may be initials (``Maria Ana MA Silva`` keeps last ``Silva``). :issue:`785`, :issue:`752` - **A Lexicon or Parser pickled by an earlier release no longer loads.** Loading one raises ``ValueError`` naming the new fields (``suffix_words_ambiguous``, ``agnomens``); re-pickle after upgrading. ``ParsedName`` and ``HumanName`` pickles still load. :issue:`752` * 2.6.0 - October 25, 2026 diff --git a/nameparser/_pipeline/_pieces.py b/nameparser/_pipeline/_pieces.py index 1cb0cad9e..c2b2cf1c7 100644 --- a/nameparser/_pipeline/_pieces.py +++ b/nameparser/_pipeline/_pieces.py @@ -1454,12 +1454,34 @@ def behind_the_merged_credential(rest: Sequence[int], p: int, # end of its part" -- applied to a finished peel (#602), so every # reader of the trailing run (assign, P5's reserve, P2's chain stop, # the maiden take) sees one answer. +def _name_word_behind(piece: Sequence[int], tokens: Sequence[WorkToken], + one_case: bool | None) -> bool: + """Whether the piece behind a member its capitals settle reads as a + word of the name rather than a further credential: a particle, or a + cased word not written in lower case (Title case or capitals, title + vocabulary included) that is no suffix word and no member its own + capitals settle. Only `run_start` asks it, of a settled member.""" + if len(piece) != 1: + return False + tok = tokens[piece[0]] + if "particle" in tok.tags: + return True + text = tok.text + if text.upper() == text.lower() or text.islower(): + return False + return ("vocab:suffix" not in tok.tags + and SHAPE_ACRONYM_TAG not in tok.tags + and not (AMBIGUOUS_ACRONYM_TAG in tok.tags + and settled_by_case(tok, one_case))) + + def run_start(rest: Sequence[int], names: int, pieces: Sequence[Sequence[int]], ptags: Sequence[Set[str]], tokens: Sequence[WorkToken], units: Sequence[int] | None = None, - one_case: bool | None = None) -> int: + one_case: bool | None = None, + initials_cue: bool = True) -> int: """Where #602's run starts in `rest[:names]`: the position of the first credential that starts one (`starts_a_credential_run`) with two name pieces in front of it, or `names` where none does -- with @@ -1507,7 +1529,16 @@ def run_start(rest: Sequence[int], names: int, # p >= 2 here: two name words stand in front and starts_a_credential_run( pieces[q], ptags[q], tokens, - tokens[pieces[rest[p - 1]][0]].text, one_case)): + tokens[pieces[rest[p - 1]][0]].text, one_case) + # rules/S8: "not where a name word stands behind it" + # -- a settled member followed by one is the + # writer's initials as likely as a credential + and not (initials_cue and p + 1 < len(rest) + and AMBIGUOUS_ACRONYM_TAG in tags + and settled_by_case(tokens[pieces[q][0]], + one_case) + and _name_word_behind(pieces[rest[p + 1]], + tokens, one_case))): return p # a lone particle is not yet a name: 'de Mesnil' is one surname # -- unless it opens a chain run, which is then the surname @@ -1820,6 +1851,7 @@ def tail_reading(rest: list[int], pieces: Sequence[Sequence[int]], units: Sequence[int] | None = None, count_numeral: bool = True, salutations: bool = False, + initials_cue: bool = True, ) -> tuple[list[int], tuple[int, ...], Peel]: """The S2 peel and the H5 chain read together to a FIXED POINT: peel, chain, splice the chained pieces out, peel again over what @@ -1912,7 +1944,7 @@ def tail_reading(rest: list[int], pieces: Sequence[Sequence[int]], end=peeled.names, salutations=salutations) if kept == peeled.names: p = run_start(rest, peeled.names, pieces, ptags, tokens, units, - one_case) + one_case, initials_cue) return rest, (), (peeled if p == peeled.names else credential_run( rest, peeled, p, pieces, ptags, tokens, one_case)) # `rest[:hi]` stands as written; `behind` holds the peeled runs @@ -1966,7 +1998,7 @@ def tail_reading(rest: list[int], pieces: Sequence[Sequence[int]], rest = rest[:hi] + [j for run in reversed(behind) for j in run] final = Peel(peeled.names, tuple(numerals), tuple(picks), None) p = run_start(rest, final.names, pieces, ptags, tokens, units, - one_case) + one_case, initials_cue) return (rest, tuple(j for run in reversed(titled) for j in run), final if p == final.names else credential_run( rest, final, p, pieces, ptags, tokens, one_case)) @@ -2237,7 +2269,8 @@ def trailing_run_start(start: int, pieces: Sequence[Sequence[int]], # acronym half is still counted here, as before #718 rest, titled, peel = tail_reading(peel_walk(start, ptags), pieces, ptags, tokens, one_case, - count_numeral=False, salutations=True) + count_numeral=False, salutations=True, + initials_cue=False) first = len(pieces) for k in rest[peel.names:]: first = min(first, k) diff --git a/tests/v2/cases.py b/tests/v2/cases.py index ec20955bf..466f7b911 100644 --- a/tests/v2/cases.py +++ b/tests/v2/cases.py @@ -5973,6 +5973,47 @@ def _check_cjk_shape_purity(self) -> None: "credential's run (the row below). 1.4.0 read suffix " "'ma, Ma'", shape=2), + Case("capitals_initials_before_the_family_name_stay_a_name", + "Maria Ana MA Silva", + {"given": "Maria", "middle": "Ana MA", "family": "Silva"}, + notes="S8 (Derek, 2026-10-26, #785): with no family comma a " + "name word behind a member its capitals settle keeps it " + "from starting the credential's run -- here the writer's own initials in capitals between the given names and the family name. " + "1.4.0 through 2.6.0 read the same"), + Case("capitals_before_a_capitals_family_name_stay_a_name", + "Jean Pierre MA DUPONT", + {"given": "Jean", "middle": "Pierre MA", "family": "DUPONT"}, + notes="S8 (Derek, 2026-10-26, #785): with no family comma a " + "name word behind a member its capitals settle keeps it " + "from starting the credential's run -- here a family name written in capitals by convention, which counts as the name word behind. " + "1.4.0 through 2.6.0 read the same"), + Case("capitals_before_a_title_word_surname_stay_a_name", + "Mary Jane MA King", + {"given": "Mary", "middle": "Jane MA", "family": "King"}, + notes="S8 (Derek, 2026-10-26, #785): with no family comma a " + "name word behind a member its capitals settle keeps it " + "from starting the credential's run -- here a surname that is also title vocabulary, which inside the run would read as a title. " + "1.4.0 through 2.6.0 read the same"), + Case("capitals_credential_before_a_family_name_is_the_accepted_cost", + "John Smith MA Jones", + {"given": "John", "middle": "Smith MA", "family": "Jones"}, + notes="S8 (Derek, 2026-10-26, #785): with no family comma a " + "name word behind a member its capitals settle keeps it " + "from starting the credential's run -- here the cost Accepted in S8, a credential written between the given names and the family name, where 'John Smith M.A. Jones' reads suffix 'M.A. Jones'. " + "1.4.0 through 2.6.0 read the same"), + Case("a_shape_admitted_numeral_keeps_its_run_under_the_caps_switch", + "John Smith VI Ma", + {"given": "John", "family": "Smith", "suffix": "VI Ma"}, + policy=Policy(unlisted_caps_suffixes=CapsSuffixes.EVERYWHERE), + classification="fix(#785)", + ambiguities=("suffix-or-name", "suffix-or-name"), + notes="S8: the caps switch writes the class tags on 'VI', " + "which its numeral reading still starts the run with; " + "the name word behind a member keeps only a member its " + "capitals settle from starting it, and the first draft of " + "#785's narrowing asked the tag instead, reading middle " + "'Smith VI', family 'Ma' here. 2.6.0 read the same as " + "this row"), Case("a_capitals_member_in_the_given_part_starts_the_run", "Doe, John MA Ma", {"given": "John", "family": "Doe", "suffix": "MA Ma"}, diff --git a/tests/v2/pipeline/test_pieces.py b/tests/v2/pipeline/test_pieces.py index 8bf9fc80c..04e6ef228 100644 --- a/tests/v2/pipeline/test_pieces.py +++ b/tests/v2/pipeline/test_pieces.py @@ -895,10 +895,19 @@ def test_a_member_its_capitals_settle_starts_the_run() -> None: # its particle reading still refuses 'DO', the clash settled being # the one with the name reading alone for written in ("MA", "M.A."): - name = parse(f"John Smith {written} Jones") - assert (name.family, name.suffix) == ("Smith", f"{written} Jones") + name = parse(f"Smith, John {written} Jones") + assert name.suffix == f"{written} Jones" assert [a.kind.value for a in name.ambiguities] == [ "suffix-or-name"] + assert parse("John Smith MA JD Jones").suffix == "MA JD Jones" + assert parse("John Smith M.A. Jones").suffix == "M.A. Jones" + # with no comma a name word behind the capitals keeps them a name + # word, as initials may be (Derek, 2026-10-26) + for text, family in (("John Smith MA Jones", "Jones"), + ("Maria Ana MA Silva", "Silva"), + ("Jean Pierre MA DUPONT", "DUPONT"), + ("Mary Jane MA King", "King")): + assert parse(text).family == family, text assert parse("John Smith DO Jones").family == "DO Jones" diff --git a/tests/v2/rules_budget.json b/tests/v2/rules_budget.json index b1f80a087..044ecf6b2 100644 --- a/tests/v2/rules_budget.json +++ b/tests/v2/rules_budget.json @@ -57,9 +57,9 @@ "S3": {"words": 960, "then_words": 398, "because_sentences": 3, "except": 4, "precedence": 5, "accepted": 6, "accepted_words": 343}, "S4": {"words": 306, "then_words": 164, "because_sentences": 2, "except": 0, "precedence": 3, "accepted": 0, "accepted_words": 0}, "S5": {"words": 731, "then_words": 400, "because_sentences": 1, "except": 2, "precedence": 1, "accepted": 0, "accepted_words": 0}, - "S6": {"words": 580, "then_words": 325, "because_sentences": 2, "except": 1, "precedence": 2, "accepted": 0, "accepted_words": 0}, + "S6": {"words": 585, "then_words": 330, "because_sentences": 2, "except": 1, "precedence": 2, "accepted": 0, "accepted_words": 0}, "S7": {"words": 602, "then_words": 388, "because_sentences": 1, "except": 2, "precedence": 2, "accepted": 1, "accepted_words": 105}, - "S8": {"words": 467, "then_words": 302, "because_sentences": 1, "except": 1, "precedence": 2, "accepted": 1, "accepted_words": 84}, + "S8": {"words": 531, "then_words": 366, "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": 474, "then_words": 266, "because_sentences": 3, "except": 1, "precedence": 1, "accepted": 2, "accepted_words": 107}, diff --git a/tests/v2/test_cases.py b/tests/v2/test_cases.py index ccd5beed1..045283916 100644 --- a/tests/v2/test_cases.py +++ b/tests/v2/test_cases.py @@ -257,9 +257,10 @@ def test_the_field_sweep_sees_the_claims_it_checks() -> None: # the orders it reaches); its new rows add their run reports, and # the rows rewritten in one case or lower case to keep reaching # their emitter add theirs back; 1310 -> 1316 the same day, the - # review's rows for the comma's opening word and the chain's stop. - assert claims == 1316, ( - f"the field sweep checks {claims} claims, recorded as 1316 on " + # review's rows for the comma's opening word and the chain's stop; + # 1316 -> 1318 on 2026-10-26, the narrowing's caps-switch numeral row. + assert claims == 1318, ( + f"the field sweep checks {claims} claims, recorded as 1318 on " f"2026-10-25") diff --git a/tests/v2/test_facade_cases.py b/tests/v2/test_facade_cases.py index f44f3c963..49c7df4a9 100644 --- a/tests/v2/test_facade_cases.py +++ b/tests/v2/test_facade_cases.py @@ -219,6 +219,7 @@ "strict_comma_reports_a_one_case_numeral_in_a_third_part", "the_comma_flip_is_read_under_the_declared_order", "two_caps_credentials_peel_as_a_run", + "a_shape_admitted_numeral_keeps_its_run_under_the_caps_switch", "the_caps_shape_is_script_agnostic_cyrillic", "the_caps_shape_is_script_agnostic_accented", "the_dotted_switch_off_still_reports_the_post_comma_fork", diff --git a/tools/differential/corpus_rules.jsonl b/tools/differential/corpus_rules.jsonl index eca153a84..e37c25c86 100644 --- a/tools/differential/corpus_rules.jsonl +++ b/tools/differential/corpus_rules.jsonl @@ -364,7 +364,9 @@ "John Smith K.C." "John Smith M. D." "John Smith M.A." +"John Smith M.A. Jones" "John Smith MA" +"John Smith MA JD Jones" "John Smith MA Jones" "John Smith MB ChB" "John Smith MD PhD" @@ -550,7 +552,6 @@ "Mai Le née Nguyen" "Malcolm X" "Mari' Aube'" -"Maria Ana M.A. Silva" "Maria Ana MA Silva" "Maria Kowalska (z domu Nowak)" "Maria Kowalska (z domu)" diff --git a/tools/differential/snapshot.jsonl b/tools/differential/snapshot.jsonl index 64bc84c40..939c51365 100644 --- a/tools/differential/snapshot.jsonl +++ b/tools/differential/snapshot.jsonl @@ -1517,7 +1517,7 @@ {"name": "Jean Marie Fils", "given": "Jean", "middle": "Marie", "family": "Fils", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. M. F."} {"name": "Jean Pierre DUPONT", "given": "Jean", "middle": "Pierre", "family": "DUPONT", "initials": "J. P. D.", "capitalized_force": "Jean Pierre Dupont"} {"name": "Jean Pierre DUPONT-MORETTI", "given": "Jean", "middle": "Pierre", "family": "DUPONT-MORETTI", "initials": "J. P. D.", "capitalized_force": "Jean Pierre Dupont-Moretti"} -{"name": "Jean Pierre MA DUPONT", "given": "Jean", "family": "Pierre", "suffix": "MA DUPONT", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. P.", "capitalized_force": "Jean Pierre MA Dupont"} +{"name": "Jean Pierre MA DUPONT", "given": "Jean", "middle": "Pierre MA", "family": "DUPONT", "initials": "J. P. M. D.", "capitalized_force": "Jean Pierre Ma Dupont"} {"name": "Jean St Clair", "given": "Jean", "family": "St Clair", "initials": "J. C.", "capitalized_force": "Jean st Clair"} {"name": "Jean de Beaumanoir de Lavardin", "given": "Jean", "middle": "de Beaumanoir", "family": "de Lavardin", "initials": "J. B. L."} {"name": "Jean de Mesnil", "given": "Jean", "family": "de Mesnil", "initials": "J. M."} @@ -1668,8 +1668,10 @@ {"name": "John Smith K.C.", "given": "John", "middle": "Smith", "family": "K.C.", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S. K."} {"name": "John Smith M. D.", "given": "John", "middle": "Smith M.", "family": "D.", "initials": "J. S. M. D."} {"name": "John Smith M.A.", "given": "John", "family": "Smith", "suffix": "M.A.", "initials": "J. S."} +{"name": "John Smith M.A. Jones", "given": "John", "family": "Smith", "suffix": "M.A. Jones", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S."} {"name": "John Smith MA", "given": "John", "family": "Smith", "suffix": "MA", "initials": "J. S."} -{"name": "John Smith MA Jones", "given": "John", "family": "Smith", "suffix": "MA Jones", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S."} +{"name": "John Smith MA JD Jones", "given": "John", "family": "Smith", "suffix": "MA JD Jones", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S."} +{"name": "John Smith MA Jones", "given": "John", "middle": "Smith MA", "family": "Jones", "initials": "J. S. M. J.", "capitalized_force": "John Smith Ma Jones"} {"name": "John Smith MB ChB", "given": "John", "family": "Smith", "suffix": "MB ChB", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "J. S."} {"name": "John Smith MC", "given": "John", "family": "Smith", "suffix": "MC", "initials": "J. S."} {"name": "John Smith MD PhD", "given": "John", "family": "Smith", "suffix": "MD PhD", "initials": "J. S."} @@ -2335,8 +2337,7 @@ {"name": "Mari-Ann Henri Roobert Tammik, parlamendiliige", "given": "parlamendiliige", "family": "Mari-Ann Henri Roobert Tammik", "initials": "p. M. H. R. T.", "capitalized_force": "Parlamendiliige Mari-Ann Henri Roobert Tammik"} {"name": "Maria", "given": "Maria", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "M."} {"name": "Maria", "order": "FAMILY_FIRST", "family": "Maria", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "M."} -{"name": "Maria Ana M.A. Silva", "given": "Maria", "family": "Ana", "suffix": "M.A. Silva", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "M. A."} -{"name": "Maria Ana MA Silva", "given": "Maria", "family": "Ana", "suffix": "MA Silva", "ambiguities": ["SUFFIX_OR_NAME"], "initials": "M. A."} +{"name": "Maria Ana MA Silva", "given": "Maria", "middle": "Ana MA", "family": "Silva", "initials": "M. A. M. S.", "capitalized_force": "Maria Ana Ma Silva"} {"name": "Maria Firmina dos Reis", "given": "Maria", "middle": "Firmina", "family": "dos Reis", "initials": "M. F. R."} {"name": "Maria Hasani", "given": "Maria", "family": "Hasani", "initials": "M. H."} {"name": "Maria Helena Fernandes", "given": "Maria", "middle": "Helena", "family": "Fernandes", "initials": "M. H. F."} @@ -2392,7 +2393,7 @@ {"name": "Mary Jane King", "given": "Mary", "middle": "Jane", "family": "King", "initials": "M. J. K."} {"name": "Mary Jane King Smith", "given": "Mary", "middle": "Jane King", "family": "Smith", "initials": "M. J. K. S."} {"name": "Mary Jane King.", "title": "King.", "given": "Mary", "family": "Jane", "initials": "M. J."} -{"name": "Mary Jane MA King", "title": "King", "given": "Mary", "family": "Jane", "suffix": "MA", "initials": "M. J."} +{"name": "Mary Jane MA King", "given": "Mary", "middle": "Jane MA", "family": "King", "initials": "M. J. M. K.", "capitalized_force": "Mary Jane Ma King"} {"name": "Mary Johnson", "given": "Mary", "family": "Johnson", "initials": "M. J."} {"name": "Mary Nicet", "given": "Mary", "family": "Nicet", "initials": "M. N."} {"name": "Mary Smith née Jones Prof.", "title": "Prof.", "given": "Mary", "family": "Smith", "maiden": "Jones", "initials": "M. S."} From 10441a38f958aa4de175c874093a7b37ed06a7c6 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Mon, 26 Oct 2026 10:53:23 -0700 Subject: [PATCH 5/6] Count a merged particle run or joined surname as the name word behind (#785) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The narrowing's review found `_name_word_behind` refusing any multi-word piece, so a particle run or connective join that group had already merged ('de la Vega', 'van der Berg', 'Ortega y Gasset') let a settled member start the run: `Maria Ana MA de la Vega` read suffix 'MA de la Vega'. It asks the piece's first word now, a credential merged across a space ('Pharm D', 'Ph. D.') still excluded, and a word of a script with no case counts as a name word (`John Smith MA 田中`). Every reading this moves on the review's 516,672-parse grid lands on master's; the local corpora move as before. Co-Authored-By: Claude Opus 5.5 --- .../S6-case-signal-at-the-suffix-slot.md | 2 +- .../rules/S8-credentials-trailing-run.md | 4 +++- nameparser/_pipeline/_pieces.py | 20 ++++++++++++------- tests/v2/cases.py | 14 +++++++++++++ tests/v2/rules_budget.json | 2 +- tools/differential/corpus_rules.jsonl | 2 ++ tools/differential/snapshot.jsonl | 2 ++ 7 files changed, 36 insertions(+), 10 deletions(-) diff --git a/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md b/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md index 1d63b4e9e..fc332d010 100644 --- a/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md +++ b/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md @@ -43,5 +43,5 @@ Open: none in this file. Recompute: `uv run python tools/differential/snapshot.py --since `; for each local corpus `snapshot.py --local --base `, and `local_agreement.py psc-individuals --base `; the ctgov shape count by reading `corpus.jsonl` of ctgov-officials, keeping each distinct name whose part before its first comma holds, after two words, a word whose letters are a listed member written in capitals between two words that each open with a capital and are not wholly in capitals, and counting those whose suffix holds it, on this tree and on the parent. - 2026-10-25 #785, review — THREE SITES THE FIRST VERSION LEFT ASKING THE OLD QUESTION, found by the PR's code review on a grid of 104,676 parses (125 shapes, 23 spellings of the member as written, in upper and in lower case, three orders, four policies) against master, and a twin grid comparing each capitals member with its dotted spelling. (i) The comma's report on the part's opening word was withheld only where every listed member of the part was settled, so `Smith, MA PhD Ma` still reported its settled `MA` beside the Title-case `Ma` S7's company reads; it asks the word's own writing now. (ii) P2's chain stopped at a suffix word and not at a settled member, so after a family comma with a title-particle head the chain took the member and the words behind it into the given name in silence (`Smith, Freiherr von Berg MA Jones` → given 'von Berg MA Jones', where `Smith, Freiherr von Berg M.A. Jones` reads suffix 'M.A. Jones'); `_pieces.chain_run_end` asks `settled_by_case` now, the chain's units with it, and the member starts the run. (iii) P5's bound given-name join took a settled member as the given name's second word (`Smith, Abdul MA` → given 'Abdul MA' in silence, where `Smith, Abdul M.A.` reads suffix 'M.A.'); it declines one now, as it declines a suffix word (decisions/P5). Measured after the fixes, against the same master: the twin grid's capitals-against-dotted disagreements fall from 4,062 on master to 2,250, every one of which master shares; the main grid's removed reports all name a settled member and no report is added; ctgov-officials, fec-individuals and tse-donors move exactly as the entry above records, the three sites reaching no name in them. The design-docs review found the API reference (`AmbiguityKind.SUFFIX_OR_NAME`'s docstring), rules/P6 and Standing lines in decisions/M2, S4 and S5 still stating the old reports, and S8's Accepted clause narrower than its cost: a family name written in capitals by convention reads the same way (`Jean Pierre MA DUPONT` → suffix 'MA DUPONT'), and a name word behind the member that is title vocabulary reads as a title in silence (`Mary Jane MA King` → title 'King', as `Mary Jane M.A. King` reads); all are amended. Left as it reads: `John Smith MA y` reads family 'MA y' where `John Smith PhD y` reads suffix 'PhD y', a trailing connective no writing produces. - 2026-10-25 #785, review of the fix — THE CHAIN STOP AND THE JOIN'S DECLINE ARE NARROWED TO THE GIVEN PART AFTER A FAMILY COMMA. SUPERSEDES the review entry's (ii) and (iii) where they read "wherever". Applied everywhere, both left a settled member where nothing then took it, read as a name word in silence: the chain stopped in front of a member the trailing read declined (`Freiherr von Berg MA Ma` → given 'von Berg', middle 'MA', family 'Ma', and under the family-first orders given 'MA'; 162 parses of the fix review's grid, the only moves of the fix away from both master and the dotted spelling), and the join declined one with no comma (`Abdul MA Smith` → middle 'MA'). After a family comma the member always has a reader, the given part's trailing slot or the run S8 starts behind the given word, so the two widenings stand there and nowhere else; outside it the readings are master's (decisions/P2, decisions/P5). The comma's opening-word guard was found sound. -- 2026-10-26 (Derek), #785 — A NAME WORD BEHIND THE CAPITALS KEEPS THEM A NAME WORD, WITH NO FAMILY COMMA. SUPERSEDES the 2026-10-25 #785 entry's ACCEPTED paragraph and its review entry's widening of that clause to capitals family names and title words. Derek, on PR #895: narrow #785's second question rather than accept its cost. In a name with no family comma, a listed member settled by its capitals does not start rules/S8's credential run where a name word stands right behind it: a particle, or a word in Title case or in capitals that is no suffix word, no word of the class by shape and no member its own capitals settle. Such a word is evidence the capitals are the writer's initials (`Maria Ana MA Silva`) or part of a name written in capitals by convention (`Jean Pierre MA DUPONT`, the capitals family name counting as the name word, a reading the narrowing decides rather than leaves open), as the design's cue table has Title case lean to the name; and a title word behind them stays the family name it is (`Mary Jane MA King`). Each of the four keeps the reading 1.4.0 through 2.6.0 gave (middle 'Ana MA', family 'Silva', and so on). The cost, Accepted in rules/S8: `John Smith MA Jones` keeps middle 'Smith MA', family 'Jones', in silence as before, where `John Smith M.A. Jones` reads suffix 'M.A. Jones'. A credential behind the member still lets it start the run (`John Smith MA JD Jones` → suffix 'MA JD Jones'), and after a family comma nothing changes: the run starts behind the given word whatever follows (`Smith, John MA Jones` → suffix 'MA Jones'), which is where ctgov's five credential-list fixes stand. `_pieces.run_start` asks `_name_word_behind` of a settled member, and the given part's predictor (`trailing_run_start`) passes `initials_cue=False` so it keeps agreeing with assign's walk there. A first draft keyed the test on the class tag, which the caps switch (`CapsSuffixes.EVERYWHERE`) also writes on a numeral it admits by shape, so `John Smith VI Ma` lost its generation reading under that switch (middle 'Smith VI', family 'Ma'); the test asks `settled_by_case` now, and a case row holds the shape. +- 2026-10-26 (Derek), #785 — A NAME WORD BEHIND THE CAPITALS KEEPS THEM A NAME WORD, WITH NO FAMILY COMMA. SUPERSEDES the 2026-10-25 #785 entry's ACCEPTED paragraph and its review entry's widening of that clause to capitals family names and title words. Derek, on PR #895: narrow #785's second question rather than accept its cost. In a name with no family comma, a listed member settled by its capitals does not start rules/S8's credential run where a name word stands right behind it: a particle, a word in a script with no case, or a word in Title case or in capitals that is no suffix word, no word of the class by shape and no member its own capitals settle, a particle run or a joined surname that group has merged counting by its first word (the narrowing's review found `Maria Ana MA de la Vega`, `Maria Ana MA Ortega y Gasset` and `John Smith MA 田中` starting the run under a first draft that asked only a one-word piece; a lower-case word does not count, as the rule was stated, so `John Smith MA jones` reads suffix 'MA jones'). Such a word is evidence the capitals are the writer's initials (`Maria Ana MA Silva`) or part of a name written in capitals by convention (`Jean Pierre MA DUPONT`, the capitals family name counting as the name word, a reading the narrowing decides rather than leaves open), as the design's cue table has Title case lean to the name; and a title word behind them stays the family name it is (`Mary Jane MA King`). Each of the four keeps the reading 1.4.0 through 2.6.0 gave (middle 'Ana MA', family 'Silva', and so on). The cost, Accepted in rules/S8: `John Smith MA Jones` keeps middle 'Smith MA', family 'Jones', in silence as before, where `John Smith M.A. Jones` reads suffix 'M.A. Jones'. A credential behind the member still lets it start the run (`John Smith MA JD Jones` → suffix 'MA JD Jones'), and after a family comma nothing changes: the run starts behind the given word whatever follows (`Smith, John MA Jones` → suffix 'MA Jones'), which is where ctgov's five credential-list fixes stand. `_pieces.run_start` asks `_name_word_behind` of a settled member, and the given part's predictor (`trailing_run_start`) passes `initials_cue=False` so it keeps agreeing with assign's walk there. A first draft keyed the test on the class tag, which the caps switch (`CapsSuffixes.EVERYWHERE`) also writes on a numeral it admits by shape, so `John Smith VI Ma` lost its generation reading under that switch (middle 'Smith VI', family 'Ma'); the test asks `settled_by_case` now, and a case row holds the shape. MEASURED (2026-10-26, Python 3.11.17, against master a46b3a6e with `--base`): ctgov-officials moves 1,772 readings, the 1,767 dropped reports of the entries above and 5 field moves, the credential lists after a comma; the 6 initials readings are gone and nothing else moves. fec-individuals, tse-donors and psc-individuals match the base, and every agreement record matches. The committed snapshot moves 55 names against master, 48 a dropped report and 7 a field, every one a case-row or rules text. On the first review's grid of 104,676 parses, master against this tree, 1,227 parses move a field, each a mixed-case name whose capitals member now reads as a suffix, every report removed names a member that ends in the suffix field, and none is added; capitals-against-dotted disagreements are 3,078, every one of which master shares. diff --git a/docs/design/rules/S8-credentials-trailing-run.md b/docs/design/rules/S8-credentials-trailing-run.md index 33639e24a..08240de3f 100644 --- a/docs/design/rules/S8-credentials-trailing-run.md +++ b/docs/design/rules/S8-credentials-trailing-run.md @@ -4,7 +4,7 @@ **Then:** a credential after the name core starts a run to the end of its part (#602): every later word of that part reads as a suffix, except a title word, which reads as a title. -- **Where the run starts.** An unambiguous suffix word of two or more letters, in any script, standing after two name words where no comma divides the name or before a suffix comma, or after the given word in the part after a family comma, makes every later word of that part a suffix. So does a roman numeral of two or more letters that reads as the generation (S10), whether the vocabulary lists it or its shape alone reads it, and so does a listed member of the ambiguous class its writing settles as the credential, written in capitals in a name of mixed case (S6) — after the given word in the part after a family comma wherever it stands, and elsewhere not where a name word stands behind it: a particle, or a word in Title case or in capitals that is no credential, which makes the capitals as likely the writer's own initials (`Maria Ana MA Silva`), or a family name written in capitals, as a credential. +- **Where the run starts.** An unambiguous suffix word of two or more letters, in any script, standing after two name words where no comma divides the name or before a suffix comma, or after the given word in the part after a family comma, makes every later word of that part a suffix. So does a roman numeral of two or more letters that reads as the generation (S10), whether the vocabulary lists it or its shape alone reads it, and so does a listed member of the ambiguous class its writing settles as the credential, written in capitals in a name of mixed case (S6) — after the given word in the part after a family comma wherever it stands, and elsewhere not where a name word stands behind it: a particle (a particle run or a joined surname counting by its first word), a word in a script with no case, or a word in Title case or in capitals that is no credential, which makes the capitals as likely the writer's own initials (`Maria Ana MA Silva`), or a family name written in capitals, as a credential. ``` "John Smith VI Jones" → suffix="VI Jones" "Smith, John MA Jones" → suffix="MA Jones" @@ -12,6 +12,8 @@ "Maria Ana MA Silva" → family="Silva" "Jean Pierre MA DUPONT" → family="DUPONT" "Mary Jane MA King" → family="King" + "Maria Ana MA de la Vega" → family="de la Vega" + "Maria Ana MA Ortega y Gasset" → family="Ortega y Gasset" "Smith, John VI Jones" → suffix="VI Jones" "Smith, John PhD Jones" → suffix="PhD Jones" "John Smith PhD Prof. Ma" → suffix="PhD Ma" diff --git a/nameparser/_pipeline/_pieces.py b/nameparser/_pipeline/_pieces.py index c2b2cf1c7..a9d6a8c1d 100644 --- a/nameparser/_pipeline/_pieces.py +++ b/nameparser/_pipeline/_pieces.py @@ -1454,20 +1454,25 @@ def behind_the_merged_credential(rest: Sequence[int], p: int, # end of its part" -- applied to a finished peel (#602), so every # reader of the trailing run (assign, P5's reserve, P2's chain stop, # the maiden take) sees one answer. -def _name_word_behind(piece: Sequence[int], tokens: Sequence[WorkToken], +def _name_word_behind(piece: Sequence[int], ptags: Set[str], + tokens: Sequence[WorkToken], one_case: bool | None) -> bool: """Whether the piece behind a member its capitals settle reads as a - word of the name rather than a further credential: a particle, or a - cased word not written in lower case (Title case or capitals, title - vocabulary included) that is no suffix word and no member its own - capitals settle. Only `run_start` asks it, of a settled member.""" - if len(piece) != 1: + word of the name rather than a further credential, asked of its + first word, since group has merged particle runs and connective + joins by then ('de la Vega', 'Ortega y Gasset'): a particle, a word + of a script with no case, or a cased word not written in lower case + (Title case or capitals, title vocabulary included), that is no + suffix word and no member its own capitals settle. A credential + merged across a space ('Pharm D', 'Ph. D.') is no name word. Only + `run_start` asks it, of a settled member.""" + if "suffix" in ptags or "split" in ptags: return False tok = tokens[piece[0]] if "particle" in tok.tags: return True text = tok.text - if text.upper() == text.lower() or text.islower(): + if text.islower(): return False return ("vocab:suffix" not in tok.tags and SHAPE_ACRONYM_TAG not in tok.tags @@ -1538,6 +1543,7 @@ def run_start(rest: Sequence[int], names: int, and settled_by_case(tokens[pieces[q][0]], one_case) and _name_word_behind(pieces[rest[p + 1]], + ptags[rest[p + 1]], tokens, one_case))): return p # a lone particle is not yet a name: 'de Mesnil' is one surname diff --git a/tests/v2/cases.py b/tests/v2/cases.py index 466f7b911..bec7fd1ab 100644 --- a/tests/v2/cases.py +++ b/tests/v2/cases.py @@ -5994,6 +5994,20 @@ def _check_cjk_shape_purity(self) -> None: "name word behind a member its capitals settle keeps it " "from starting the credential's run -- here a surname that is also title vocabulary, which inside the run would read as a title. " "1.4.0 through 2.6.0 read the same"), + Case("capitals_before_a_particle_surname_stay_a_name", + "Maria Ana MA de la Vega", + {"given": "Maria", "middle": "Ana MA", "family": "de la Vega"}, + notes="S8 (Derek, 2026-10-26, #785): a particle run the chain " + "has merged counts as the name word behind by its first " + "word; a first draft asked only one-word pieces and read " + "suffix 'MA de la Vega'. 1.4.0 through 2.6.0 read the " + "same as this row"), + Case("capitals_before_a_joined_surname_stay_a_name", + "Maria Ana MA Ortega y Gasset", + {"given": "Maria", "middle": "Ana MA", "family": "Ortega y Gasset"}, + notes="S8 (Derek, 2026-10-26, #785): a connective join counts " + "by its first word, as the particle run above. 2.6.0 " + "read the same as this row"), Case("capitals_credential_before_a_family_name_is_the_accepted_cost", "John Smith MA Jones", {"given": "John", "middle": "Smith MA", "family": "Jones"}, diff --git a/tests/v2/rules_budget.json b/tests/v2/rules_budget.json index 044ecf6b2..51606d82f 100644 --- a/tests/v2/rules_budget.json +++ b/tests/v2/rules_budget.json @@ -59,7 +59,7 @@ "S5": {"words": 731, "then_words": 400, "because_sentences": 1, "except": 2, "precedence": 1, "accepted": 0, "accepted_words": 0}, "S6": {"words": 585, "then_words": 330, "because_sentences": 2, "except": 1, "precedence": 2, "accepted": 0, "accepted_words": 0}, "S7": {"words": 602, "then_words": 388, "because_sentences": 1, "except": 2, "precedence": 2, "accepted": 1, "accepted_words": 105}, - "S8": {"words": 531, "then_words": 366, "because_sentences": 1, "except": 1, "precedence": 2, "accepted": 1, "accepted_words": 63}, + "S8": {"words": 551, "then_words": 386, "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": 474, "then_words": 266, "because_sentences": 3, "except": 1, "precedence": 1, "accepted": 2, "accepted_words": 107}, diff --git a/tools/differential/corpus_rules.jsonl b/tools/differential/corpus_rules.jsonl index e37c25c86..043c36d64 100644 --- a/tools/differential/corpus_rules.jsonl +++ b/tools/differential/corpus_rules.jsonl @@ -552,7 +552,9 @@ "Mai Le née Nguyen" "Malcolm X" "Mari' Aube'" +"Maria Ana MA Ortega y Gasset" "Maria Ana MA Silva" +"Maria Ana MA de la Vega" "Maria Kowalska (z domu Nowak)" "Maria Kowalska (z domu)" "Maria Kowalska (z, domu Nowak)" diff --git a/tools/differential/snapshot.jsonl b/tools/differential/snapshot.jsonl index 939c51365..02645005a 100644 --- a/tools/differential/snapshot.jsonl +++ b/tools/differential/snapshot.jsonl @@ -2337,7 +2337,9 @@ {"name": "Mari-Ann Henri Roobert Tammik, parlamendiliige", "given": "parlamendiliige", "family": "Mari-Ann Henri Roobert Tammik", "initials": "p. M. H. R. T.", "capitalized_force": "Parlamendiliige Mari-Ann Henri Roobert Tammik"} {"name": "Maria", "given": "Maria", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "M."} {"name": "Maria", "order": "FAMILY_FIRST", "family": "Maria", "ambiguities": ["GIVEN_OR_FAMILY"], "initials": "M."} +{"name": "Maria Ana MA Ortega y Gasset", "given": "Maria", "middle": "Ana MA", "family": "Ortega y Gasset", "initials": "M. A. M. O. G.", "capitalized_force": "Maria Ana Ma Ortega y Gasset"} {"name": "Maria Ana MA Silva", "given": "Maria", "middle": "Ana MA", "family": "Silva", "initials": "M. A. M. S.", "capitalized_force": "Maria Ana Ma Silva"} +{"name": "Maria Ana MA de la Vega", "given": "Maria", "middle": "Ana MA", "family": "de la Vega", "initials": "M. A. M. V.", "capitalized_force": "Maria Ana Ma de la Vega"} {"name": "Maria Firmina dos Reis", "given": "Maria", "middle": "Firmina", "family": "dos Reis", "initials": "M. F. R."} {"name": "Maria Hasani", "given": "Maria", "family": "Hasani", "initials": "M. H."} {"name": "Maria Helena Fernandes", "given": "Maria", "middle": "Helena", "family": "Fernandes", "initials": "M. H. F."} From 151b0621b9fe7f8ba266e6b209427d68d2486976 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Mon, 26 Oct 2026 11:09:52 -0700 Subject: [PATCH 6/6] Read the Ph./D. pair behind a capitals credential as a credential (#785) Codecov showed the name-word test's split-credential exclusion unexercised. Writing the case that reaches it turned up the Ph./D. pair, which the walk leaves out: the test saw the word behind the pair as the word behind the member, so `John Smith MA Ph. D. Jones` read family 'MA', suffix 'Ph. D. Jones' (as master does) where its dotted twin reads suffix 'M.A. Ph. D. Jones'. `run_start` asks `behind_the_merged_credential` first now, and `John Smith MA Pharm D Jones` is pinned beside it. The helper moves above `run_start`'s S8 comment, which it had split from its function. Co-Authored-By: Claude Opus 5.5 --- .../decisions/S6-case-signal-at-the-suffix-slot.md | 2 +- nameparser/_pipeline/_pieces.py | 12 ++++++++---- tests/v2/cases.py | 12 ++++++++++++ tests/v2/pipeline/test_pieces.py | 4 ++++ tests/v2/test_cases.py | 7 ++++--- 5 files changed, 29 insertions(+), 8 deletions(-) diff --git a/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md b/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md index fc332d010..941e4c852 100644 --- a/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md +++ b/docs/design/decisions/S6-case-signal-at-the-suffix-slot.md @@ -43,5 +43,5 @@ Open: none in this file. Recompute: `uv run python tools/differential/snapshot.py --since `; for each local corpus `snapshot.py --local --base `, and `local_agreement.py psc-individuals --base `; the ctgov shape count by reading `corpus.jsonl` of ctgov-officials, keeping each distinct name whose part before its first comma holds, after two words, a word whose letters are a listed member written in capitals between two words that each open with a capital and are not wholly in capitals, and counting those whose suffix holds it, on this tree and on the parent. - 2026-10-25 #785, review — THREE SITES THE FIRST VERSION LEFT ASKING THE OLD QUESTION, found by the PR's code review on a grid of 104,676 parses (125 shapes, 23 spellings of the member as written, in upper and in lower case, three orders, four policies) against master, and a twin grid comparing each capitals member with its dotted spelling. (i) The comma's report on the part's opening word was withheld only where every listed member of the part was settled, so `Smith, MA PhD Ma` still reported its settled `MA` beside the Title-case `Ma` S7's company reads; it asks the word's own writing now. (ii) P2's chain stopped at a suffix word and not at a settled member, so after a family comma with a title-particle head the chain took the member and the words behind it into the given name in silence (`Smith, Freiherr von Berg MA Jones` → given 'von Berg MA Jones', where `Smith, Freiherr von Berg M.A. Jones` reads suffix 'M.A. Jones'); `_pieces.chain_run_end` asks `settled_by_case` now, the chain's units with it, and the member starts the run. (iii) P5's bound given-name join took a settled member as the given name's second word (`Smith, Abdul MA` → given 'Abdul MA' in silence, where `Smith, Abdul M.A.` reads suffix 'M.A.'); it declines one now, as it declines a suffix word (decisions/P5). Measured after the fixes, against the same master: the twin grid's capitals-against-dotted disagreements fall from 4,062 on master to 2,250, every one of which master shares; the main grid's removed reports all name a settled member and no report is added; ctgov-officials, fec-individuals and tse-donors move exactly as the entry above records, the three sites reaching no name in them. The design-docs review found the API reference (`AmbiguityKind.SUFFIX_OR_NAME`'s docstring), rules/P6 and Standing lines in decisions/M2, S4 and S5 still stating the old reports, and S8's Accepted clause narrower than its cost: a family name written in capitals by convention reads the same way (`Jean Pierre MA DUPONT` → suffix 'MA DUPONT'), and a name word behind the member that is title vocabulary reads as a title in silence (`Mary Jane MA King` → title 'King', as `Mary Jane M.A. King` reads); all are amended. Left as it reads: `John Smith MA y` reads family 'MA y' where `John Smith PhD y` reads suffix 'PhD y', a trailing connective no writing produces. - 2026-10-25 #785, review of the fix — THE CHAIN STOP AND THE JOIN'S DECLINE ARE NARROWED TO THE GIVEN PART AFTER A FAMILY COMMA. SUPERSEDES the review entry's (ii) and (iii) where they read "wherever". Applied everywhere, both left a settled member where nothing then took it, read as a name word in silence: the chain stopped in front of a member the trailing read declined (`Freiherr von Berg MA Ma` → given 'von Berg', middle 'MA', family 'Ma', and under the family-first orders given 'MA'; 162 parses of the fix review's grid, the only moves of the fix away from both master and the dotted spelling), and the join declined one with no comma (`Abdul MA Smith` → middle 'MA'). After a family comma the member always has a reader, the given part's trailing slot or the run S8 starts behind the given word, so the two widenings stand there and nowhere else; outside it the readings are master's (decisions/P2, decisions/P5). The comma's opening-word guard was found sound. -- 2026-10-26 (Derek), #785 — A NAME WORD BEHIND THE CAPITALS KEEPS THEM A NAME WORD, WITH NO FAMILY COMMA. SUPERSEDES the 2026-10-25 #785 entry's ACCEPTED paragraph and its review entry's widening of that clause to capitals family names and title words. Derek, on PR #895: narrow #785's second question rather than accept its cost. In a name with no family comma, a listed member settled by its capitals does not start rules/S8's credential run where a name word stands right behind it: a particle, a word in a script with no case, or a word in Title case or in capitals that is no suffix word, no word of the class by shape and no member its own capitals settle, a particle run or a joined surname that group has merged counting by its first word (the narrowing's review found `Maria Ana MA de la Vega`, `Maria Ana MA Ortega y Gasset` and `John Smith MA 田中` starting the run under a first draft that asked only a one-word piece; a lower-case word does not count, as the rule was stated, so `John Smith MA jones` reads suffix 'MA jones'). Such a word is evidence the capitals are the writer's initials (`Maria Ana MA Silva`) or part of a name written in capitals by convention (`Jean Pierre MA DUPONT`, the capitals family name counting as the name word, a reading the narrowing decides rather than leaves open), as the design's cue table has Title case lean to the name; and a title word behind them stays the family name it is (`Mary Jane MA King`). Each of the four keeps the reading 1.4.0 through 2.6.0 gave (middle 'Ana MA', family 'Silva', and so on). The cost, Accepted in rules/S8: `John Smith MA Jones` keeps middle 'Smith MA', family 'Jones', in silence as before, where `John Smith M.A. Jones` reads suffix 'M.A. Jones'. A credential behind the member still lets it start the run (`John Smith MA JD Jones` → suffix 'MA JD Jones'), and after a family comma nothing changes: the run starts behind the given word whatever follows (`Smith, John MA Jones` → suffix 'MA Jones'), which is where ctgov's five credential-list fixes stand. `_pieces.run_start` asks `_name_word_behind` of a settled member, and the given part's predictor (`trailing_run_start`) passes `initials_cue=False` so it keeps agreeing with assign's walk there. A first draft keyed the test on the class tag, which the caps switch (`CapsSuffixes.EVERYWHERE`) also writes on a numeral it admits by shape, so `John Smith VI Ma` lost its generation reading under that switch (middle 'Smith VI', family 'Ma'); the test asks `settled_by_case` now, and a case row holds the shape. +- 2026-10-26 (Derek), #785 — A NAME WORD BEHIND THE CAPITALS KEEPS THEM A NAME WORD, WITH NO FAMILY COMMA. SUPERSEDES the 2026-10-25 #785 entry's ACCEPTED paragraph and its review entry's widening of that clause to capitals family names and title words. Derek, on PR #895: narrow #785's second question rather than accept its cost. In a name with no family comma, a listed member settled by its capitals does not start rules/S8's credential run where a name word stands right behind it: a particle, a word in a script with no case, or a word in Title case or in capitals that is no suffix word, no word of the class by shape and no member its own capitals settle, a particle run or a joined surname that group has merged counting by its first word (the narrowing's review found `Maria Ana MA de la Vega`, `Maria Ana MA Ortega y Gasset` and `John Smith MA 田中` starting the run under a first draft that asked only a one-word piece; a lower-case word does not count, as the rule was stated, so `John Smith MA jones` reads suffix 'MA jones'). The Ph./D. pair, which the walk leaves out, is no name word either: `John Smith MA Ph. D. Jones` reads suffix 'MA Ph. D. Jones' as its dotted twin does, where master read family 'MA' (found when codecov showed the split-credential exclusion unexercised). Such a word is evidence the capitals are the writer's initials (`Maria Ana MA Silva`) or part of a name written in capitals by convention (`Jean Pierre MA DUPONT`, the capitals family name counting as the name word, a reading the narrowing decides rather than leaves open), as the design's cue table has Title case lean to the name; and a title word behind them stays the family name it is (`Mary Jane MA King`). Each of the four keeps the reading 1.4.0 through 2.6.0 gave (middle 'Ana MA', family 'Silva', and so on). The cost, Accepted in rules/S8: `John Smith MA Jones` keeps middle 'Smith MA', family 'Jones', in silence as before, where `John Smith M.A. Jones` reads suffix 'M.A. Jones'. A credential behind the member still lets it start the run (`John Smith MA JD Jones` → suffix 'MA JD Jones'), and after a family comma nothing changes: the run starts behind the given word whatever follows (`Smith, John MA Jones` → suffix 'MA Jones'), which is where ctgov's five credential-list fixes stand. `_pieces.run_start` asks `_name_word_behind` of a settled member, and the given part's predictor (`trailing_run_start`) passes `initials_cue=False` so it keeps agreeing with assign's walk there. A first draft keyed the test on the class tag, which the caps switch (`CapsSuffixes.EVERYWHERE`) also writes on a numeral it admits by shape, so `John Smith VI Ma` lost its generation reading under that switch (middle 'Smith VI', family 'Ma'); the test asks `settled_by_case` now, and a case row holds the shape. MEASURED (2026-10-26, Python 3.11.17, against master a46b3a6e with `--base`): ctgov-officials moves 1,772 readings, the 1,767 dropped reports of the entries above and 5 field moves, the credential lists after a comma; the 6 initials readings are gone and nothing else moves. fec-individuals, tse-donors and psc-individuals match the base, and every agreement record matches. The committed snapshot moves 55 names against master, 48 a dropped report and 7 a field, every one a case-row or rules text. On the first review's grid of 104,676 parses, master against this tree, 1,227 parses move a field, each a mixed-case name whose capitals member now reads as a suffix, every report removed names a member that ends in the suffix field, and none is added; capitals-against-dotted disagreements are 3,078, every one of which master shares. diff --git a/nameparser/_pipeline/_pieces.py b/nameparser/_pipeline/_pieces.py index a9d6a8c1d..dd5ac90fd 100644 --- a/nameparser/_pipeline/_pieces.py +++ b/nameparser/_pipeline/_pieces.py @@ -1450,10 +1450,6 @@ def behind_the_merged_credential(rest: Sequence[int], p: int, and "suffix" in ptags[rest[p] - 1]) -# rules/S8: "A credential after the name core starts a run to the -# end of its part" -- applied to a finished peel (#602), so every -# reader of the trailing run (assign, P5's reserve, P2's chain stop, -# the maiden take) sees one answer. def _name_word_behind(piece: Sequence[int], ptags: Set[str], tokens: Sequence[WorkToken], one_case: bool | None) -> bool: @@ -1480,6 +1476,10 @@ def _name_word_behind(piece: Sequence[int], ptags: Set[str], and settled_by_case(tok, one_case))) +# rules/S8: "A credential after the name core starts a run to the +# end of its part" -- applied to a finished peel (#602), so every +# reader of the trailing run (assign, P5's reserve, P2's chain stop, +# the maiden take) sees one answer. def run_start(rest: Sequence[int], names: int, pieces: Sequence[Sequence[int]], ptags: Sequence[Set[str]], @@ -1539,6 +1539,10 @@ def run_start(rest: Sequence[int], names: int, # -- a settled member followed by one is the # writer's initials as likely as a credential and not (initials_cue and p + 1 < len(rest) + # the Ph./D. pair behind it is a credential + # the walk leaves out (#827) + and not behind_the_merged_credential( + rest, p + 1, ptags) and AMBIGUOUS_ACRONYM_TAG in tags and settled_by_case(tokens[pieces[q][0]], one_case) diff --git a/tests/v2/cases.py b/tests/v2/cases.py index bec7fd1ab..d6488e2fa 100644 --- a/tests/v2/cases.py +++ b/tests/v2/cases.py @@ -6008,6 +6008,18 @@ def _check_cjk_shape_purity(self) -> None: notes="S8 (Derek, 2026-10-26, #785): a connective join counts " "by its first word, as the particle run above. 2.6.0 " "read the same as this row"), + Case("capitals_before_a_split_phd_start_the_run", + "John Smith MA Ph. D. Jones", + {"given": "John", "family": "Smith", "suffix": "MA Ph. D. Jones"}, + classification="fix(#785)", + ambiguities=("suffix-or-name",), + notes="S8: the Ph./D. pair behind the settled MA is a " + "credential the walk leaves out, not the name word that " + "would keep MA a name, so MA starts the run as 'John " + "Smith M.A. Ph. D. Jones' reads; the first draft of the " + "narrowing asked the next word the walk holds, 'Jones'. " + "2.6.0 read middle 'Smith', family 'MA', suffix 'Ph. D. " + "Jones'"), Case("capitals_credential_before_a_family_name_is_the_accepted_cost", "John Smith MA Jones", {"given": "John", "middle": "Smith MA", "family": "Jones"}, diff --git a/tests/v2/pipeline/test_pieces.py b/tests/v2/pipeline/test_pieces.py index 04e6ef228..e2cad9b78 100644 --- a/tests/v2/pipeline/test_pieces.py +++ b/tests/v2/pipeline/test_pieces.py @@ -900,6 +900,10 @@ def test_a_member_its_capitals_settle_starts_the_run() -> None: assert [a.kind.value for a in name.ambiguities] == [ "suffix-or-name"] assert parse("John Smith MA JD Jones").suffix == "MA JD Jones" + # a credential split across a space behind it is no name word, + # merged into one piece ('Pharm D') or left out of the walk (Ph. D.) + assert parse("John Smith MA Pharm D Jones").suffix == "MA Pharm D Jones" + assert parse("John Smith MA Ph. D. Jones").suffix == "MA Ph. D. Jones" assert parse("John Smith M.A. Jones").suffix == "M.A. Jones" # with no comma a name word behind the capitals keeps them a name # word, as initials may be (Derek, 2026-10-26) diff --git a/tests/v2/test_cases.py b/tests/v2/test_cases.py index 045283916..ea7dd035a 100644 --- a/tests/v2/test_cases.py +++ b/tests/v2/test_cases.py @@ -258,9 +258,10 @@ def test_the_field_sweep_sees_the_claims_it_checks() -> None: # the rows rewritten in one case or lower case to keep reaching # their emitter add theirs back; 1310 -> 1316 the same day, the # review's rows for the comma's opening word and the chain's stop; - # 1316 -> 1318 on 2026-10-26, the narrowing's caps-switch numeral row. - assert claims == 1318, ( - f"the field sweep checks {claims} claims, recorded as 1318 on " + # 1316 -> 1318 on 2026-10-26, the narrowing's caps-switch numeral row; + # 1318 -> 1321 the same day, its Ph./D. row. + assert claims == 1321, ( + f"the field sweep checks {claims} claims, recorded as 1321 on " f"2026-10-25")