Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md

Large diffs are not rendered by default.

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/design/decisions/comma-suffix-arc.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ its decline-by-measurement evidence in the issue body; "z domu" folded into it 2

Not standing: plan amendment 1's repair, carried out by #296's first commit — 2026-08-01.

Elsewhere: the audit table's dispositions as shipped, `md` kept a dual (until #853) and `ms` and `sa` kept out of the ambiguous set — decisions/C8's 2026-08-23 #296 and 2026-08-23 #428 Standing lines, the second also holding the repair's condition, no name word; the trailing slot read by vocabulary, a period-marked title word there a title (`John Smith Prof.` → title 'Prof.') — decisions/H5's 2026-09-08 #316 entry; the dotted shape class — decisions/S9's 2026-09-14 (Derek) entry headed #516's DOTTED HALF IS A SWITCH.
Elsewhere: the audit table's dispositions as shipped, `md` kept a dual (until #853) and `ms` and `sa` kept out of the ambiguous set (`sa` until 2026-10-25, #857, when it left the vocabulary) — decisions/C8's 2026-08-23 #296 and 2026-08-23 #428 Standing lines, the second also holding the repair's condition, no name word; the trailing slot read by vocabulary, a period-marked title word there a title (`John Smith Prof.` → title 'Prof.') — decisions/H5's 2026-09-08 #316 entry; the dotted shape class — decisions/S9's 2026-09-14 (Derek) entry headed #516's DOTTED HALF IS A SWITCH.

Open: none in this file.

## Log

- 2026-07-26 #291/#296/#316 (its audit table's `ms` and `sa` joining the ambiguous set superseded by decisions/C8's 2026-08-23 (review of PR #428, four reviewers) entry; its trailing-abbreviation decline superseded for the dotted spelling by decisions/S9's 2026-09-14 (Derek) entry headed #516's DOTTED HALF IS A SWITCH) — THE COMMA-SUFFIX ARC, 2026-07-26 → 2026-08-01: the paragraph, the plan amendment and the Declined entries below.
- 2026-07-26 #291/#296/#316 (its audit table's `ms` and `sa` joining the ambiguous set superseded by decisions/C8's 2026-08-23 (review of PR #428, four reviewers) entry (`sa` left the vocabulary instead on 2026-10-25, #857, decisions/C8); its trailing-abbreviation decline superseded for the dotted spelling by decisions/S9's 2026-09-14 (Derek) entry headed #516's DOTTED HALF IS A SWITCH) — THE COMMA-SUFFIX ARC, 2026-07-26 → 2026-08-01: the paragraph, the plan amendment and the Declined entries below.

The approved 19-word TITLES∩suffix audit table (2026-07-30/08-01, intent for the unshipped bundle): jr, junior, phd, md, do and se DROP from TITLES (se: Structural Engineer is the PE/SE post-nominal pair, no prenominal convention — v1 residue); dr drops from SUFFIX_WORDS and sra from the suffix sets (v1 residue); sr, lt, cpl, cpt, cpo, csm, sgm, ra and vc KEEP dual membership (position decides); ms and sa keep dual membership AND join the ambiguous set (the periods gate). Derek's framing thesis for the whole table: v1's sets encoded where the v1 PARSER needed words to be, not where words can occur. Amendment A6: once SUFFIX_PHRASES ships, the glued peel's token-level post-nominal test cannot step over a phrase segment ("김민준씨, LEED AP" will not peel) — #291 owns deciding that, with a case row either way.

Expand Down
8 changes: 4 additions & 4 deletions docs/design/decisions/suffix-acronym-collisions.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/design/decisions/vocabulary-exclusions.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Excluded (the never-given / ambiguous particle line, a particle reading's `may_b
- Encoding rationale (#293, predating #360's membership questions): the data layer stores the NEVER-GIVEN set and derives the ambiguous one, because that is safe-by-default for new particles — a one-place addition — and the v1 shim translates by one-directional complement. And the constants are FROZEN specifically to kill the cached-Lexicon.default()-vs-fresh- Constants desync that runtime module-constant mutation caused. The data-layer half is superseded since 2026-10-18 (decisions/vocabulary-model): every particle reading states `may_be_given`, true or false, and neither side is a default; the freeze half stands.
- Load-bearing dependency: TITLES ∩ ambiguous — {do, freiherr, st} until #296's audit, {freiherr, st} since — is what keeps the particle-or-given ambiguity emitter reachable at all; moving them all would leave the shipped vocabulary unable to reach it, which is why test_the_chained_emitter_is_reachable_by_construction (tests/v2/test_parser.py) pins reachability by construction rather than by shipped vocabulary — an empty intersection fails no test and does not mean the emitter is unreachable.

Open (contested vocabulary memberships — the rule is right, the word's set is questioned; the issue is canonical): none. The block held three entries and all three were answered inside a fortnight — #346, #343 and #344 together on 2026-09-06 (see #indic-honorifics) and #342 on 2026-09-07 (see #suffix-acronym-collisions). The heading stays with nothing under it on purpose, the same reason an empty ledger roster section is a statement and a missing one is nobody having looked. sa, se and om are named as the next candidates by #342's own comment and carry no issue, so they are not entries here.
Open (contested vocabulary memberships — the rule is right, the word's set is questioned; the issue is canonical): none. The block held three entries and all three were answered inside a fortnight — #346, #343 and #344 together on 2026-09-06 (see #indic-honorifics) and #342 on 2026-09-07 (see #suffix-acronym-collisions). The heading stays with nothing under it on purpose, the same reason an empty ledger roster section is a statement and a missing one is nobody having looked. sa, se and om (all three since decided, sa on 2026-10-25, #857) are named as the next candidates by #342's own comment and carry no issue, so they are not entries here.

Excluded (SUFFIX_ACRONYMS — esq, removed 2026-09-08, #316/#489 bundle):

Expand Down
1 change: 1 addition & 0 deletions docs/release_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Release Log
**Behavior Changes**

- **Fix credentials that are also surnames reading as credentials.** ``Luo Si`` and ``Robert Capa`` give a family name, where 2.0.0 through 2.5.0 gave suffix ``Si`` or ``Capa`` with no family name. A Title-case word after a full name takes the family name, as ``Ma`` does; the all-caps form stays a credential (``John Smith CAPA`` keeps suffix ``CAPA``). :issue:`791`, :issue:`796`
- **Fix Sa reading as a credential or a title.** ``Maria de Sa`` and ``MARIA DE SA`` give family ``de Sa`` and ``DE SA``, and ``Joao Sa`` gives family ``Sa``, where 1.4.0 through 2.5.0 gave suffix ``Sa`` (``Joao Sa``: 2.0.0 through 2.5.0); ``Mrs Sa Li`` gives given ``Sa``, where it read ``Sa`` as a second title. ``SA`` is no longer a credential: ``John Smith SA`` gives family ``SA`` and ``John Smith, SA`` gives given ``SA``, family ``John Smith``, where 1.4.0 through 2.5.0 gave suffix ``SA``; a caller who needs it adds it to their own lexicon. :issue:`857`
- **Fix more credentials after a comma reading as the given name.** ``John Smith, MBChB`` and ``John Smith, OTR/L`` give suffix ``MBChB`` and ``OTR/L``, where 2.0.0 through 2.5.0 gave given ``MBChB`` and family ``John Smith``; 37 degrees and certifications are listed, and ``PA`` and ``Ing`` are marked ambiguous (``John Pa`` and ``John Ing`` keep the family name). ``Ing.`` after a full name and a comma gives suffix ``Ing.``, where 2.5.0 gave title ``Ing.``. An unlisted hyphen- or slash-joined credential still reverses. :issue:`797`, :issue:`848`
- **Fix case repair writing a joined credential like a hyphenated name.** ``john smith, kchs/dchs`` repairs to ``John Smith KCHS/DCHS``, where 2.0.0 through 2.5.0 gave ``Kchs/Dchs``; a listed acronym joined by a hyphen or slash is written in capitals as one word, and a casing the caller records for it wins. Hyphenated names such as ``Smith-Jones`` keep their per-part casing. :issue:`808`
- **Add Cllr as a title.** ``Cllr John Smith`` gives title ``Cllr``, first ``John``, last ``Smith``, where 1.4.0 through 2.5.0 gave first ``Cllr``, middle ``John``. A bare ``Cllr`` after the name is still a name word. :issue:`847`
Expand Down
2 changes: 0 additions & 2 deletions nameparser/config/_compiled.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,6 @@
'rouva',
'royal',
'rt',
'sa',
'sailor',
'saint',
'sainte',
Expand Down Expand Up @@ -2068,7 +2067,6 @@
'rtrp',
'rvm',
'rvt',
'sa',
'same',
'sasm',
'sccp',
Expand Down
5 changes: 3 additions & 2 deletions nameparser/vocab/en.toml
Original file line number Diff line number Diff line change
Expand Up @@ -759,8 +759,9 @@ military = { same_as = "radm" }

[[word]]
text = "sa"
military = { expands = "Seaman Apprentice" }
credential = { note = "NOT may_be_name, for the reason recorded on ms (decisions/C8)" }
family = { us_census_2010 = 1600, wikidata = 182, as_of = 2026-10-24, note = "The Census writes no diacritics, so its 1,600 is Sa and Sá together (37.88% Asian/PI, so much of it is Korean, Chinese or Vietnamese Sa); Wikidata is a lower bound: 110 as Sá (Portugal, Brazil) plus 72 as Sa" }
given = { us_ssa = 73, wikidata = 2, as_of = 2026-10-24, note = "US SSA births summed over both sexes; Wikidata is a lower bound; nam_dict band 5 (banded in 3 countries: Korea, Vietnam, China)" }
excluded = { military = { reason = "Seaman Apprentice, with no bearer in any corpus: every name read with SA as its title in the committed and local corpora is a person whose given name is Sa (Sa Liu, Mohamad Sa Haichal) or the Arabic Sa ad (Sa ad Hariri), or whose Portuguese surname Sá is written without its accent (Ricardo Sa Pinto); as a title it also took the slot after a family comma and ahead of a name", issue = 857, decided = 2026-10-25 }, credential = { reason = "no person in the corpora writes it: the corpora's SA and S.A. suffixes are companies (Galderma S.A.), the South African qualifier in FCP(SA), and the initials S.A. typed without stops (MANN, S.A.); the Portuguese surname Sá written without its accent (Maria de Sa) is the reading to protect, and as a credential it needed the may_be_name flag, which left a capitals name ending in SA a suffix (MARIA DE SA). NPPES lists 167 US healthcare providers who write it, unread by item", issue = 857, decided = 2026-10-25 } }

[[word]]
text = "scpo"
Expand Down
92 changes: 85 additions & 7 deletions tests/v2/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -10575,13 +10575,91 @@ def _check_cjk_shape_purity(self) -> None:
Case("audit_ms_before_a_name_after_comma_is_the_title",
"Smith, Ms. Jane",
{"title": "Ms.", "given": "Jane", "family": "Smith"}),
Case("audit_sa_after_comma_is_the_postnominal", "Smith, SA",
{"family": "Smith", "suffix": "SA"},
classification="fix(#296)",
notes="the same dual: Special Agent leading, the business "
"form trailing"),
Case("audit_perioded_sa_is_the_postnominal", "Smith, S.A.",
{"family": "Smith", "suffix": "S.A."}),
# #857: sa is in NO vocabulary set -- not a credential, not a title
# (its military and credential readings are both excluded on the
# word). It is the Portuguese surname Sa written without its accent,
# so it reads as any unlisted word does, whatever the case. These
# rows pin that decision against the listed twin `ma`, which stays a
# credential in the same shapes: an unlisted all-caps word is a
# credential by SHAPE only behind a comma (S9), and the period
# spellings go through the unlisted-dotted-suffix rule (S3).
Case("sa_after_a_particle_is_the_surname", "Maria de Sa",
{"given": "Maria", "family": "de Sa"},
classification="fix(#857)"),
Case("bare_sa_is_a_surname", "Joao Sa",
{"given": "Joao", "family": "Sa"}),
Case("capitals_sa_after_a_particle_is_the_surname", "MARIA DE SA",
{"given": "MARIA", "family": "DE SA"},
classification="fix(#857)",
notes="the 255 SA / SA pairs of tse-donors (#843) read alike"),
Case("capitals_ma_after_a_particle_is_the_credential", "MARIA DE MA",
{"given": "MARIA", "family": "DE", "suffix": "MA"},
ambiguities=("suffix-or-name",),
notes="the listed twin of the row above"),
Case("lowercase_sa_after_a_particle_is_the_surname", "maria de sa",
{"given": "maria", "family": "de sa"},
classification="fix(#857)"),
Case("capitals_sa_with_words_to_spare_is_the_surname", "ANA SILVA SA",
{"given": "ANA", "middle": "SILVA", "family": "SA"},
classification="fix(#857)"),
Case("capitals_sa_with_no_word_to_spare_is_the_surname", "ANA SA",
{"given": "ANA", "family": "SA"}),
Case("sa_after_a_middle_name_is_the_surname", "Maria Silva Sa",
{"given": "Maria", "middle": "Silva", "family": "Sa"},
classification="fix(#857)"),
Case("sa_before_a_family_comma_is_the_surname", "Sa, Maria",
{"given": "Maria", "family": "Sa"}),
Case("sa_in_front_is_a_name_word", "SA Smith",
{"given": "SA", "family": "Smith"},
classification="fix(#857)",
notes="not a title: Seaman Apprentice has no bearer in any "
"corpus (decisions/C8, #857)"),
Case("mrs_then_sa_is_one_title", "Mrs Sa Li",
{"title": "Mrs", "given": "Sa", "family": "Li"},
classification="fix(#857)",
notes="`sa` was a second title word and took the given name"),
Case("sa_after_a_family_comma_is_the_given_name", "Smith, SA",
{"given": "SA", "family": "Smith"},
classification="fix(#857)",
notes="2.5.0 read suffix 'SA' (#296's audit row); no person "
"writes it as a credential in any corpus (#857)"),
Case("sa_after_a_family_comma_and_a_name_is_a_middle_name",
"Smith, SA John",
{"given": "SA", "middle": "John", "family": "Smith"},
classification="fix(#857)"),
Case("mixed_case_sa_after_a_full_name_is_the_family_name",
"John Smith SA",
{"given": "John", "middle": "Smith", "family": "SA"},
classification="fix(#857)",
notes="accepted cost (#857): no shape reads an unlisted "
"all-caps tail as a credential at this slot (S9), so "
"with no vocabulary behind it SA is the family name"),
Case("sa_after_a_full_name_and_a_comma_merges_the_name",
"John Smith, SA",
{"given": "SA", "family": "John Smith"},
classification="fix(#857)",
notes="accepted cost (#857): the reading an unlisted two-letter "
"caps word gets after a full name; 1.4.0 through 2.5.0 "
"read suffix 'SA'. The listed twin `John Smith, MA` "
"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",)),
Case("sa_after_a_comma_with_no_head_is_the_given_name", ", SA",
{"given": "SA"},
classification="fix(#857)",
notes="1.4.0 read title 'SA'; 2.5.0 suffix 'SA'"),
Case("sa_ending_the_given_part_is_a_middle_name", "Smith, John SA",
{"given": "John", "middle": "SA", "family": "Smith"},
classification="fix(#857)"),
Case("sa_after_a_credential_joins_it", "Smith, John PhD Sa",
{"given": "John", "family": "Smith", "suffix": "PhD Sa"},
ambiguities=("suffix-or-name",)),
Case("perioded_sa_after_a_family_comma_is_unlisted",
"Smith, S.A.",
{"given": "S.A.", "family": "Smith"},
ambiguities=("suffix-or-name",)),
# MOVED by #289, not deleted: 'DO' is written in capitals inside a
# mixed-case name, so it now leans CREDENTIAL and the post-comma
# slot takes it with one word to spare rather than falling to the
Expand Down
12 changes: 8 additions & 4 deletions tests/v2/test_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,14 @@ def test_the_field_sweep_sees_the_claims_it_checks() -> None:
# 1399 -> 1400 the same day: #754's 'Beethoven Ludwig Maria van'
# row, which carries its own policy, one particle-or-given claim;
# 1400 -> 1403 the same day: #835's particle row 'Doe, Jane van
# Mrs', one particle-or-given claim under each order.
assert claims == 1403, (
f"the field sweep checks {claims} claims, recorded as 1403 on "
f"2026-10-24")
# Mrs', one particle-or-given claim under each order; 1403 -> 1406
# the same day: #857's sa rows, after the word left every set (the
# one reporting row, 'Smith, S.A.', claims under the orders it
# reaches); 1406 -> 1415 the same day: the review's comma rows for
# sa and the listed `ma` twins, three of them reporting.
assert claims == 1415, (
f"the field sweep checks {claims} claims, recorded as 1415 on "
f"2026-10-25")


def test_every_case_name_is_unique() -> None:
Expand Down
8 changes: 4 additions & 4 deletions tests/v2/test_local_corpora.py
Original file line number Diff line number Diff line change
Expand Up @@ -3671,19 +3671,19 @@ def test_the_pairs_view_counts_the_pairs_whose_spellings_share_a_surname() -> No


def test_the_pairs_view_lists_the_accents_only_pairs_read_with_two_surnames() -> None:
# SA is a credential and SÁ is not, so the readings part; JÚNIOR and
# RA is a credential and RÁ is not, so the readings part; JÚNIOR and
# JUNIOR are both suffixes (#843), and the accent on JOSÉ moves
# nothing. Fewer than ten: not listed.
found = local.pair_counts(_paired(*[("ANA SÁ", "ANA SA")] * 10,
found = local.pair_counts(_paired(*[("ANA SILVA RÁ", "ANA SILVA RA")] * 10,
*[("JOSE SILVA JÚNIOR", "JOSÉ SILVA JUNIOR")] * 10,
*[("JOSE SILVA", "JOSÉ SILVA")] * 10,
*[("JOSE SILVA MÁ", "JOSÉ SILVA MA")] * 9),
agreement=True)
assert found.agreement == {"accents only": (20, 39)}
assert found.accent_splits == {"SÁ / SA": 10, "MÁ / MA": 9}
assert found.accent_splits == {"RÁ / RA": 10, "MÁ / MA": 9}
report = local.format_pairs(found, top=15)
assert ("accents-only pairs read with two surnames, by their last words: "
"SÁ / SA 10") in report
"RÁ / RA 10") in report
assert "MÁ / MA" not in report


Expand Down
Loading
Loading