1from nameparser.config._invariants import assert_normalized
2
3SUFFIX_WORDS = frozenset({
4 # #269: Cyrillic мл/ст (junior/senior, the jr/sr analogs) deferred
5 # pending the within-script collision vetting the issue asks for;
6 # 'ст' especially is a plausible false-positive risk (many two-
7 # letter Cyrillic abbreviations exist), and both are short enough
8 # to worry about. Not shipped in this pass.
9 'esq',
10 'esquire',
11 'jr',
12 'jnr',
13 'junior',
14 'sr',
15 'snr',
16 '2',
17 'i',
18 'ii',
19 'iii',
20 'iv',
21 'v',
22 # Bare, not '(ret)'/'(vet)': moved here from literal parenthesized
23 # entries in SUFFIX_ACRONYMS. parse_nicknames()'s handle_match() now
24 # strips parens/quotes before this set is consulted, so the bare form
25 # is correct -- do not re-add the parenthesized form, that would
26 # silently reintroduce the #111 bug (parenthesized "(Ret)" matching
27 # literally instead of going through nickname/suffix disambiguation).
28 'ret',
29 'vet',
30
31 # #269 follow-up: Hebrew post-nominals, both gershayim spellings
32 # (ASCII '"' and U+05F4); the mid-word quote is inert in
33 # extraction, like the ד"ר title. Neither is ever a name.
34 'ז"ל', # "of blessed memory" (deceased), ASCII quote
35 'ז״ל', # same, U+05F4 gershayim
36 'שליט"א', # honorific for a living rabbi, ASCII quote
37 'שליט״א', # same, U+05F4 gershayim
38
39 # #307/#308: CJK postnominal honorifics and degrees. Matched
40 # whole-token here, which reaches the SPACED forms; the GLUED
41 # forms (田中さん, 김민준씨) are reached by the peel #308 adds to
42 # _pipeline/_script_segment.py: it splits a listed tail off the
43 # last name token and hands the piece back to this vocabulary --
44 # so every GLUED_HONORIFICS entry below is also an entry here,
45 # asserted at the foot of this module.
46 # Self-selecting like the Korean surnames: a Han, kana or hangul
47 # entry can only ever match CJK text. Vetting per the мл/ст
48 # standard above, and note the two bars are different -- an entry
49 # can be safe in the spaced position and unsafe glued, never the
50 # other way round, because the spaced position is a token boundary
51 # its writer drew:
52 # - 氏: a rare Japanese surname reading exists, but a bare
53 # trailing 氏 after a name is the news-style honorific, and a
54 # 氏-surnamed person writes it FIRST.
55 # - 양/군: 양 is also a top-tier surname (Yang) -- but a surname
56 # LEADS, so the trailing-only suffix gate never sees it there
57 # (양 미선 keeps family 양); as single-syllable GIVEN names in
58 # final position both are vanishingly rare in practice.
59 # - 博士: an attested Japanese given name (ひろし, Hiroshi) --
60 # the doctorate reading vastly dominates the spaced trailing
61 # position this set matches.
62 # - 殿: some ninety Japanese surnames end in it (鵜殿, 真殿), which
63 # is what keeps it out of GLUED_HONORIFICS below -- but the
64 # surname-LEADS argument that clears 양/군 clears it here too.
65 # - 님: a single hangul syllable, the risk class 양/군 are in, but
66 # it has no hanja reading at all, so it cannot sit inside a
67 # Sino-Korean given name.
68 # Bare hangul 선생/교수 are deliberately absent (only the -님
69 # honorific forms ship): the bare forms read as common nouns as
70 # readily as address terms. 박사 is the exception among the three,
71 # shipping bare as well as with -님, because it names a degree
72 # rather than a role -- but all three -님 forms ship together
73 # (선생님, 교수님, 박사님 are the standard professional honorifics,
74 # and shipping two of the three was an oversight). Further
75 # candidates (여사, 太太) wait on the same case-by-case argument.
76 '씨', # ko Mr./Ms. -- standardly spaced in Korean orthography
77 '박사', # ko doctorate holder ("Dr.")
78 '박사님', # ko the same, honorific form
79 '선생님', # ko teacher/respected elder
80 '교수님', # ko professor (honorific form)
81 '군', # ko young man ("Master")
82 '양', # ko young woman ("Miss")
83 '님', # ko -- the bare honorific of online/formal address
84 '先生', # zh Mr. / ja teacher-master -- honorific in both
85 '女士', # zh Ms./Madam
86 '小姐', # zh Miss
87 '博士', # zh+ja doctorate holder, shared Han
88 '教授', # zh+ja professor, shared Han
89 '様', # ja formal Mr./Ms. (the mail-addressing honorific)
90 '氏', # ja news-style Mr. (田中氏)
91 '殿', # ja formal, official/rank-flavored
92 'さん', # ja the everyday honorific, kana
93 'さま', # ja the kana spelling of 様
94 'くん', # ja the kana spelling of 君
95 'ちゃん', # ja familiar/diminutive
96 # #346/#344/#343: spaced trailing honorifics, South Asian and
97 # Tibetan. SPACED ONLY -- none of these is in GLUED_HONORIFICS
98 # below, and none may be added to it. जी is the reason and the
99 # reason generalizes: Banerjee, Mukherjee and Chatterjee are
100 # written बनर्जी, मुखर्जी, चटर्जी, so a glued peel would strand a
101 # fragment on a bare virama (बनर् + जी) -- the 殿 criterion in a
102 # non-CJK script. गांधीजी therefore stays unpeeled, which is the
103 # accepted trade (decisions.md#indic-honorifics).
104 # NOTE the leading/trailing split across the two scripts, which is
105 # real and must not be "harmonized": Devanagari बाबू is a LEADING
106 # honorific -- a TITLES entry (#344), not a suffix -- while Bengali
107 # বাবু is TRAILING (অমল বাবু). Different codepoints, so the two
108 # entries cannot interact.
109 'rinpoche', # bo postpositional (Sogyal Rinpoche, Lama Zopa Rinpoche)
110 'जी', # hi/mr the universal respect particle (मोदी जी)
111 'साहब', # hi Sahib
112 'साहिब', # hi/pa the Sahib spelling with the i-matra
113 'साहेब', # mr the Marathi Saheb spelling
114 'महाराज', # hi Maharaj, trailing; the leading महाराजा is a TITLES entry (#344)
115 'সাহেব', # bn Saheb (রহমান সাহেব)
116 'বাবু', # bn Babu -- TRAILING; see the note above
117 'মহারাজ', # bn Maharaj
118})
119"""
120
121Post-nominal suffixes matched as WORDS: the lookup uses the normalized token,
122so only EDGE periods come off and interior ones survive -- "Junior." matches
123here, "J.u.n.o.r." does not and stays name text ("John J.u.n.o.r." parses a
124family name, on both APIs). The example is deliberately not "J.u.n.i.o.r.",
125which fails this lookup too and is a suffix anyway: an interior-period token
126that no whole-token set claims goes to ``period_joined_vocab``, which splits
127it on its periods and, no chunk being a title, calls the whole thing a
128suffix if ANY chunk is suffix vocabulary -- and the chunk "i" is the Roman
129numeral listed above.
130So membership here is not the last word on a dotted token; the sentence is
131about this set's lookup alone. :data:`SUFFIX_ACRONYMS` is the set matched
132with every period removed, so it alone covers a multi-dot spelling: both
133"P.H.D." and the bare "PhD" reach its `phd` entry, the two normalizing to
134the same string once the periods come off. The two sets are asserted
135DISJOINT (see the guard block at the bottom): a post-nominal belongs to
136one of them or the other, and which one holds it is what decides whether
137its multi-dot spelling reaches a whole-token lookup at all --
138``period_joined_vocab`` may still claim the token chunk by chunk, as the
139"J.u.n.i.o.r." example above shows.
140
141"""
142GLUED_HONORIFICS = frozenset({
143 # #308: the entries above that may also be peeled off the END of a
144 # name token -- 田中さん, 山田太郎様, 김민준씨. A separate set, not
145 # SUFFIX_WORDS reused, because the glued position has no token
146 # boundary to lean on: the vetting question is not "is this a
147 # name?" but "can this END a name?", and only entries that can
148 # never end one belong here.
149 # kana -- name-final never, in any of the four, and the kana/kanji
150 # split is itself a vetting result: くん ships where 君 cannot,
151 # since 王君 is a complete Chinese name while the hiragana spelling
152 # is unavailable to Chinese at all. Scoped to Chinese on purpose --
153 # hiragana of course spells Japanese GIVEN names (高橋みなみ is one,
154 # pinned in the case table), which is why the four entries above
155 # are vetted one at a time as never name-FINAL rather than waved
156 # through as kana.
157 'さん', 'さま', 'くん', 'ちゃん',
158 # Han -- two-character honorifics with no name-final reading in
159 # either language, plus 様. 殿 is deliberately absent though it
160 # ships spaced above; see the exclusions below.
161 '様', '先生', '教授', '女士', '小姐',
162 # hangul -- the -님 compounds too: longest-first peels 선생님 off
163 # 김선생님 whole, rather than leaving 김선생 to segment into a
164 # family 김 and a given 선생, and 박사님 off 김민준박사님 rather
165 # than stranding 박사 in the given name. 박사 is safe glued where
166 # its Han twin 博士 is not: that collision is Japanese (博士 =
167 # ひろし) and the hangul spelling carries none of it.
168 '씨', '님', '선생님', '교수님', '박사', '박사님',
169})
170"""
171
172The subset of :data:`SUFFIX_WORDS` a name token may end WITH, peeled off as
173its own token before segmentation (#308). Deliberately harsher than the
174spaced set, because a glued tail has no writer-drawn token boundary to lean
175on -- these entries are recognized in the SPACED position only:
176
177* 양, 군 -- 김지양 and 김지군 are given names ending in these syllables, and
178 양 is a top-tier surname besides.
179* 氏 -- 王氏 is a historical name form ("the Wang woman").
180* 博士 -- glued 田中博士 IS Tanaka Hiroshi, an attested given name.
181* 殿 -- some ninety Japanese surnames END in it, 鵜殿 (Udono) and 真殿
182 (Madono) with four-figure populations, so peeling it would cut a real
183 family name in two. Spaced 殿 is safe for the reason 양/군 are: a
184 殿-surnamed person's name LEADS, and the suffix gate is trailing-only.
185* The Indic trailing set (जी, साहब, साहिब, साहेब, महाराज, সাহেব, বাবু,
186 মহারাজ) and Latin rinpoche are spaced-only for the same reason 殿 is:
187 जी ends Banerjee/Mukherjee/Chatterjee (बनर्जी, मुखर्जी, चटर्जी) and a
188 glued peel would strand बनर् on a bare virama. The criterion is not
189 CJK-specific, which is the point (decisions.md#indic-honorifics).
190
191Three more are in NEITHER set, so neither spelling is recognized. 君: 王君 is
192a complete Chinese name (君 is a common given-name final), so the honorific
193reading never gets the benefit of the doubt -- while its kana spelling くん
194ships glued, above. Bare 선생 and 교수: they read as common nouns as readily
195as address terms, and only their -님 forms ship.
196
197"""
198SUFFIX_ACRONYMS_AMBIGUOUS = frozenset({
199 # Suffix acronyms that also commonly work as given-name nicknames on
200 # their own (e.g. "Ed", "JD"). Two readers in 2.x, not the single v1
201 # one this comment used to name: _extract._suffix_shaped, deciding
202 # whether parenthesized/quoted content is a nickname or a suffix
203 # (content matching one of these stays a nickname, the more common
204 # reading in ambiguous, delimiter-only context), and _vocab's
205 # suffix_as_written, which excludes the ambiguous subset from plain
206 # acronym membership so the period gate below is not dead code.
207 # _classify also tags membership as "vocab:suffix-ambiguous".
208 #
209 # When adding a new entry to SUFFIX_ACRONYMS, also add it here only if
210 # the exact letter sequence could plausibly be someone's name on its
211 # own -- a given name or nickname (e.g. 'jd', 'ed') or a common
212 # surname (e.g. 'ma', 'do'). Unambiguous certifications/degrees
213 # (e.g. 'mba', 'cpa', 'phd') don't need an entry. In 2.0 this set
214 # also gates bare recognition: an ambiguous acronym counts as a
215 # suffix only when written with periods ('M.A.' yes, 'Ma' no), so
216 # 'Jack Ma' keeps its family name.
217 #
218 # The other half of the criterion, added 2026-09-07 with #342.
219 # Being borne at all is only the entry ticket; what decides among
220 # the three answers is a comparison of FREQUENCIES -- how common
221 # the word is as a borne name in the TRAILING position against how
222 # common it is as a credential. Roughly balanced earns the marking
223 # here, and the parse reports the fork: 'ba' is the entry that
224 # earned it that day, BA being a common credential and Ba a real
225 # surname (Vietnamese; Senegalese Fula) about as common as the
226 # credential, which is the ma/Ma shape exactly. Where the NAME
227 # reading dominates, the entry is REMOVED from SUFFIX_ACRONYMS
228 # instead of marked: 'rai' and 'cha' left the set that day, both
229 # far more common as surnames than their credentials are as
230 # credentials (RAI is "RETA Authorized Instructor", CHA is
231 # Certified Hotel Administrator or Certified Healthcare Auditor,
232 # both tenuous or specialized; Rai is a common surname across
233 # Hindi- and Bengali-speaking regions and Cha the Korean 차).
234 # Where the CREDENTIAL dominates, the entry stays unambiguous.
235 # LENGTH is a correlate and not the test -- a short acronym is
236 # more often a common credential AND more often a name -- so do
237 # not read the letter counts here as a rule.
238 #
239 # Removal takes the DOTTED spelling with it too, except by
240 # accident: "John Smith R.A.I." still reads suffix 'R.A.I.' only
241 # because rules.md#S3 splits an interior-period token on its
242 # periods and the chunk 'i' happens to be a Roman numeral in
243 # SUFFIX_WORDS. "John Smith R.A.X." reads family, and so does
244 # "John Smith C.H.A." after the removal. Do not count on a dotted
245 # spelling surviving a removal. A caller who needs an entry back
246 # adds it -- Lexicon.default().add(suffix_acronyms={"cha"}) --
247 # which is the answer this library gives for every
248 # locale-specific vocabulary. The cost is stated and accepted:
249 # with the entry gone, "John Smith RAI" reads family 'RAI'. See
250 # decisions.md#suffix-acronym-collisions.
251 #
252 # NOT 'ms' or 'sa', though #296's audit table put them here for the
253 # leading-title collision (bare "Ms" the honorific, "M.S." the
254 # degree): the gate is position-blind and the collision is not.
255 # Gated, 'John Smith, MS' lost its suffix-comma route and read
256 # title 'MS', and 'Smith, Ms.' passed the gate on its one period
257 # and read as a credential anyway. Both words are genuine duals --
258 # title and unambiguous suffix -- and position decides, as for
259 # 'sr' and 'lt' (decisions.md#C1).
260 #
261 # NOT 'se' or 'om' either, weighed 2026-09-07 with #342 and left
262 # alone: no surname evidence worth standing behind, and OM is the
263 # Order of Merit. And NOT 'mc' or 'vd', which are also PARTICLES:
264 # #454 closed by design -- neither is a borne name, so a bare
265 # trailing one is the decoration, and rules.md#P6's Accepted
266 # clause names them as the two words whose positional reading
267 # does not hold.
268 'ba',
269 'do',
270 'ed',
271 'jd',
272 'ma',
273})
274"""
275
276Acronym suffixes from SUFFIX_ACRONYMS that also plausibly collide with a
277common given-name nickname. Not a partition of SUFFIX_ACRONYMS -- a small,
278standalone exception list, read by the delimited-content escape in
279``_pipeline/_extract.py`` and by ``_pipeline/_vocab.py``'s period gate.
280
281"""
282SUFFIX_ACRONYMS = frozenset({
283 '8-vsb',
284 'aas',
285 'aba',
286 'abc',
287 # "All But Dissertation". Also the Latin transliteration of the
288 # Arabic bound given-name word in BOUND_GIVEN_NAMES (whose
289 # Arabic-script counterpart عبد is in that set only) -- the one
290 # word in both sets, kept in both deliberately. Position decides
291 # at the two ends but not in a family comma's given slot; see
292 # decisions.md#P5.
293 'abd',
294 'abpp',
295 'abr',
296 'aca',
297 'acas',
298 'ace',
299 'acha',
300 'acp',
301 'ae',
302 'ae',
303 'aem',
304 'afasma',
305 'afc',
306 'afc',
307 'afm',
308 'afm',
309 'agsf',
310 'aia',
311 'aicp',
312 'ala',
313 'alc',
314 'alp',
315 'am',
316 'amd',
317 'ame',
318 'amieee',
319 'ams',
320 'aphr',
321 'apn',
322 'aprn',
323 'apr',
324 'apss',
325 'aqp',
326 'arm',
327 'arrc',
328 'asa',
329 'asc',
330 'asid',
331 'asla',
332 'asp',
333 'atc',
334 'awb',
335 'ba',
336 'bca',
337 'bcl',
338 'bcss',
339 'bds',
340 'bem',
341 'bls-i',
342 'bn',
343 'bpe',
344 'bpi',
345 'bpt',
346 'bsc',
347 'bt',
348 'btcs',
349 'bts',
350 'cacts',
351 'cae',
352 'caha',
353 'caia',
354 'cams',
355 'cap',
356 'capa',
357 'capm',
358 'capp',
359 'caps',
360 'caro',
361 'cas',
362 'casp',
363 'cb',
364 'cbe',
365 'cbm',
366 'cbne',
367 'cbnt',
368 'cbp',
369 'cbrte',
370 'cbs',
371 'cbsp',
372 'cbt',
373 'cbte',
374 'cbv',
375 'cca',
376 'ccc',
377 'ccca',
378 'cccm',
379 'cce',
380 'cchp',
381 'ccie',
382 'ccim',
383 'cciso',
384 'ccm',
385 'ccmt',
386 'ccna',
387 'ccnp',
388 'ccp',
389 'ccp-c',
390 'ccpr',
391 'ccs',
392 'ccufc',
393 'cd',
394 'cdal',
395 'cdfm',
396 'cdmp',
397 'cds',
398 'cdt',
399 'cea',
400 'ceas',
401 'cebs',
402 'ceds',
403 'ceh',
404 'cela',
405 'cem',
406 'cep',
407 'cera',
408 'cet',
409 'cfa',
410 'cfc',
411 'cfcc',
412 'cfce',
413 'cfcm',
414 'cfe',
415 'cfeds',
416 'cfi',
417 'cfm',
418 'cfp',
419 'cfps',
420 'cfr',
421 'cfre',
422 'cga',
423 'cgap',
424 'cgb',
425 'cgc',
426 'cgfm',
427 'cgfo',
428 'cgm',
429 'cgm',
430 'cgma',
431 'cgp',
432 'cgr',
433 'cgsp',
434 'ch',
435 'chba',
436 'chdm',
437 'che',
438 'ches',
439 'chfc',
440 'chfc',
441 'chi',
442 'chmc',
443 'chmm',
444 'chp',
445 'chpa',
446 'chpe',
447 'chpln',
448 'chpse',
449 'chrm',
450 'chsc',
451 'chse',
452 'chse-a',
453 'chsos',
454 'chss',
455 'cht',
456 'cia',
457 'cic',
458 'cie',
459 'cig',
460 'cip',
461 'cipm',
462 'cips',
463 'ciro',
464 'cisa',
465 'cism',
466 'cissp',
467 'cla',
468 'clsd',
469 'cltd',
470 'clu',
471 'cm',
472 'cma',
473 'cmas',
474 'cmc',
475 'cmfo',
476 'cmg',
477 'cmp',
478 'cms',
479 'cmsp',
480 'cmt',
481 'cna',
482 'cnm',
483 'cnp',
484 'cp',
485 'cp-c',
486 'cpa',
487 'cpacc',
488 'cpbe',
489 'cpcm',
490 'cpcu',
491 'cpe',
492 'cpfa',
493 'cpfo',
494 'cpg',
495 'cph',
496 'cpht',
497 'cpim',
498 'cpl',
499 'cplp',
500 'cpm',
501 'cpo',
502 'cpp',
503 'cppm',
504 'cprc',
505 'cpre',
506 'cprp',
507 'cpsc',
508 'cpsi',
509 'cpss',
510 'cpt',
511 'cpwa',
512 'crde',
513 'crisc',
514 'crma',
515 'crme',
516 'crna',
517 'cro',
518 'crp',
519 'crt',
520 'crtt',
521 'csa',
522 'csbe',
523 'csc',
524 'cscp',
525 'cscu',
526 'csep',
527 'csi',
528 'csm',
529 'csp',
530 'cspo',
531 'csre',
532 'csrte',
533 'csslp',
534 'cssm',
535 'cst',
536 'cste',
537 'ctbs',
538 'ctfa',
539 'cto',
540 'ctp',
541 'cts',
542 'cua',
543 'cusp',
544 'cva',
545 'cva[22]',
546 'cvo',
547 'cvp',
548 'cvrs',
549 'cwap',
550 'cwb',
551 'cwdp',
552 'cwep',
553 'cwna',
554 'cwne',
555 'cwp',
556 'cwsp',
557 'cxa',
558 'cyds',
559 'cysa',
560 'dabfm',
561 'dabvlm',
562 'dacvim',
563 'dbe',
564 'dc',
565 'dcb',
566 'dcm',
567 'dcmg',
568 'dcvo',
569 'dd',
570 'dds',
571 'ded',
572 'dep',
573 'dfc',
574 'dfm',
575 'diplac',
576 'diplom',
577 'djur',
578 'dma',
579 'dmd',
580 'dmin',
581 'dnp',
582 'do',
583 'dpm',
584 'dpt',
585 'drb',
586 'drmp',
587 'drph',
588 'dsc',
589 'dsm',
590 'dso',
591 'dss',
592 'dtr',
593 'dvep',
594 'dvm',
595 'ea',
596 'ed',
597 'edd',
598 'ei',
599 'eit',
600 'els',
601 'emd',
602 'emt-b',
603 'emt-i/85',
604 'emt-i/99',
605 'emt-p',
606 'enp',
607 'erd',
608 'evp',
609 'faafp',
610 'faan',
611 'faap',
612 'fac-c',
613 'facc',
614 'facd',
615 'facem',
616 'facep',
617 'facha',
618 'facofp',
619 'facog',
620 'facp',
621 'facph',
622 'facs',
623 'faia',
624 'faicp',
625 'fala',
626 'fashp',
627 'fasid',
628 'fasla',
629 'fasma',
630 'faspen',
631 'fca',
632 'fcas',
633 'fcela',
634 'fd',
635 'fec',
636 'fhames',
637 'fic',
638 'ficf',
639 'fieee',
640 'fmp',
641 'fmva',
642 'fnss',
643 'fp&a',
644 'fp-c',
645 'fpc',
646 'frm',
647 'fsa',
648 'fsdp',
649 'fws',
650 'gaee[14]',
651 'gba',
652 'gbe',
653 'gc',
654 'gcb',
655 'gcb',
656 'gchs',
657 'gcie',
658 'gcmg',
659 'gcmg',
660 'gcsi',
661 'gcvo',
662 'gcvo',
663 'gisp',
664 'git',
665 'gm',
666 'gmb',
667 'gmr',
668 'gphr',
669 'gri',
670 'grp',
671 'gsmieee',
672 'hccp',
673 'hrs',
674 'iaccp',
675 'iaee',
676 'iccm-d',
677 'iccm-f',
678 'idsm',
679 'ifgict',
680 'iom',
681 'ipep',
682 'ipm',
683 'iso',
684 'issp-csp',
685 'issp-sa',
686 'itil',
687 'jd',
688 'jp',
689 'kbe',
690 'kcb',
691 'kchs/dchs',
692 'kcie',
693 'kcie',
694 'kcmg',
695 'kcsi',
696 'kcsi',
697 'kcvo',
698 'kg',
699 'khs/dhs',
700 'kp',
701 'kt',
702 'lac',
703 'lcmt',
704 'lcpc',
705 'lcsw',
706 'lg',
707 'litk',
708 'litl',
709 'litp',
710 'llm',
711 'lm',
712 'lmsw',
713 'lmt',
714 'lp',
715 'lpa',
716 'lpc',
717 'lpn',
718 'lpss',
719 'lsi',
720 'lsit',
721 'lt',
722 'lvn',
723 'lvo',
724 'lvt',
725 'ma',
726 'maaa',
727 'mai',
728 'mba',
729 'mbe',
730 'mbs',
731 'mc',
732 'mcct',
733 'mcdba',
734 'mches',
735 'mcm',
736 'mcp',
737 'mcpd',
738 'mcsa',
739 'mcsd',
740 'mcse',
741 'mct',
742 'md',
743 'mda',
744 'mdb',
745 'mdbb',
746 'mdep',
747 'mdhb',
748 'mdiv',
749 'mdl',
750 'mem',
751 'meng',
752 'mfa',
753 'micp',
754 'mieee',
755 'mirm',
756 'mle',
757 'mls',
758 'mlse',
759 'mlt',
760 'mm',
761 'mmad',
762 'mmas',
763 'mnaa',
764 'mnae',
765 'mp',
766 'mpa',
767 'mph',
768 'mpse',
769 'mra',
770 'ms',
771 'msa',
772 'msc',
773 'mscmsm',
774 'msm',
775 'mt',
776 'mts',
777 'mvo',
778 'nbc-his',
779 'nbcch',
780 'nbcch-ps',
781 'nbcdch',
782 'nbcdch-ps',
783 'nbcfch',
784 'nbcfch-ps',
785 'nbct',
786 'ncarb',
787 'nccp',
788 'ncidq',
789 'ncps',
790 'ncso',
791 'ncto',
792 'nd',
793 'ndtr',
794 'nmd',
795 'np',
796 'np[18]',
797 'nraemt',
798 'nremr',
799 'nremt',
800 'nrp',
801 'obe',
802 'obi',
803 'oca',
804 'ocm',
805 'ocp',
806 'od',
807 'om',
808 'oscp',
809 'ot',
810 'pa-c',
811 'pcc',
812 'pci',
813 'pe',
814 'pfmp',
815 'pg',
816 'pgmp',
817 'ph',
818 'pharmd',
819 'phc',
820 'phd',
821 'phr',
822 'phrca',
823 'pla',
824 'pls',
825 'pmc',
826 'pmi-acp',
827 'pmp',
828 'pp',
829 'pps',
830 'prm',
831 'psm',
832 'psp',
833 'psyd',
834 'pt',
835 'pta',
836 'qam',
837 'qc',
838 'qcsw',
839 'qfsm',
840 'qgm',
841 'qpm',
842 'qsd',
843 'qsp',
844 'ra',
845 'rba',
846 'rci',
847 'rcp',
848 'rd',
849 'rdcs',
850 'rdh',
851 'rdms',
852 'rdn',
853 'res',
854 'rfp',
855 'rhca',
856 'rid',
857 'rls',
858 'rmsks',
859 'rn',
860 'rp',
861 'rpa',
862 'rph',
863 'rpl',
864 'rrc',
865 'rrt',
866 'rrt-accs',
867 'rrt-nps',
868 'rrt-sds',
869 'rtrp',
870 'rvm',
871 'rvt',
872 'sa',
873 'same',
874 'sasm',
875 'sccp',
876 'scmp',
877 'se',
878 'secb',
879 'sfp',
880 'sgm',
881 'shrm-cp',
882 'shrm-scp',
883 'si',
884 'siie',
885 'smieee',
886 'sphr',
887 'sscp',
888 'stb',
889 'stmieee',
890 'tbr-ct',
891 'td',
892 'thd',
893 'thm',
894 'ud',
895 'usa',
896 'usaf',
897 'usar',
898 'uscg',
899 'usmc',
900 'usn',
901 'usnr',
902 'uxc',
903 'uxmc',
904 'vc',
905 'vc',
906 'vcp',
907 'vd',
908 'vrd',
909})
910"""
911
912Post-nominal acronyms. Titles, degrees and other things people stick after their name
913that may or may not have periods between the letters. The parser removes periods
914when matching against these pieces.
915
916"""
917
918
919# Guard the invariants the docstrings above promise, so a future edit that
920# breaks them fails at import time instead of silently drifting until a test
921# happens to catch it (same rationale as particles.py). Note `assert` is
922# stripped under `python -O`; Lexicon re-checks the relationships at
923# construction, which is what protects a caller's own vocabulary.
924assert SUFFIX_ACRONYMS_AMBIGUOUS <= SUFFIX_ACRONYMS, \
925 "SUFFIX_ACRONYMS_AMBIGUOUS must stay a subset of SUFFIX_ACRONYMS"
926# The two sets normalize differently -- the word test strips only edge
927# periods, the acronym test strips all of them -- so a word in both is
928# matched twice by two rules, and which one fired is unreadable from the
929# outside. The single overlap was 'esq', dropped 2026-09-08
930# (decisions.md#suffix-acronym-collisions), and the assert is what keeps
931# a bulk import from quietly re-creating one. It guards the SHIPPED sets
932# only: Lexicon has no matching invariant, so a caller who wants the
933# overlap in their own vocabulary may still have it.
934#
935# It carries the AMBIGUOUS set's stake too, which is the sharper one:
936# suffix_as_written ORs the word branch and the acronym branch, so an
937# ambiguous acronym that were also a suffix word would be claimed
938# through the word membership and bypass the period gate the ambiguous
939# set exists to impose. The ambiguous set is a subset of the acronyms
940# (the assert above), so this one covers it -- a separate assert of
941# `SUFFIX_ACRONYMS_AMBIGUOUS & SUFFIX_WORDS` cannot fail while both of
942# these hold.
943assert not (SUFFIX_ACRONYMS & SUFFIX_WORDS), \
944 "a post-nominal belongs to one set or the other, never both (the " \
945 "two normalize differently, and the word branch would bypass an " \
946 "ambiguous acronym's period gate): " \
947 f"{sorted(SUFFIX_ACRONYMS & SUFFIX_WORDS)}"
948# The peel splits its tail off as a TOKEN and suffix classification is
949# what claims it downstream, so a tail that is not also a suffix word
950# would split the name and then leave the piece sitting in it. The
951# reverse direction is deliberately unguarded: a suffix word that is
952# not a tail is the ordinary case, and an empty tail set is inert
953# rather than wrong.
954assert GLUED_HONORIFICS <= SUFFIX_WORDS, \
955 "GLUED_HONORIFICS must stay a subset of SUFFIX_WORDS: " \
956 f"{sorted(GLUED_HONORIFICS - SUFFIX_WORDS)}"
957assert_normalized("suffix", SUFFIX_ACRONYMS | SUFFIX_WORDS)
958
959
960# 1.x name, deprecated in 2.2 and removed in 3.0 (#293). The constant
961# did not change module, so this aliases a name to one of this module's
962# own globals: a module __getattr__ runs only once the body has finished
963# and the module is in sys.modules, so the lookup resolves rather than
964# recursing.
965from typing import TYPE_CHECKING # noqa: E402
966
967from nameparser.config._deprecated import alias_getattr # noqa: E402
968
969# Declared for the type checker, served by __getattr__ at runtime --
970# the split is what keeps mypy checking this module's LIVE names; see
971# the note in titles.py and alias_getattr's docstring.
972if TYPE_CHECKING:
973 SUFFIX_NOT_ACRONYMS: frozenset[str]
974else:
975 __getattr__, __dir__ = alias_getattr(__name__, {
976 "SUFFIX_NOT_ACRONYMS": (
977 "nameparser.config.suffixes", "SUFFIX_WORDS"),
978 })
979
980# Star imports read __all__ and never the module __getattr__ -- see the
981# note in prefixes.py. Live constants listed alongside the retired name
982# for the same reason titles.py lists its own.
983#
984# In SOURCE order, not alphabetical: `automodule :members:` follows
985# __all__ where a module defines one, so an alphabetical list here would
986# silently reorder this module's entries in modules.html. The retired
987# name goes last because autodoc does not document it, and so it has no
988# position to preserve. Not for want of SEEING it: the module member
989# scan walks dir(), which our __dir__ lists the retired name in, and
990# then calls safe_getattr on it -- so an html build resolves this name
991# and titles.py's retired one alike, emitting a real DeprecationWarning
992# for each. (Invisible in a "build succeeded, 0 warnings" line: Sphinx
993# warnings and Python warnings are different channels. Wrap
994# sphinx.cmd.build.build_main in warnings.catch_warnings to see them.)
995# What declines it is the attribute-doc scan: ModuleAnalyzer parses the
996# SOURCE and finds no assignment statement for a name served by
997# __getattr__, so autodoc computes is_attr=False, and at module level a
998# member that is not an attribute and is neither a class nor a callable
999# matches no object type at all -- no documenter is chosen and the
1000# member is skipped.
1001__all__ = [
1002 "SUFFIX_WORDS",
1003 "GLUED_HONORIFICS",
1004 "SUFFIX_ACRONYMS_AMBIGUOUS",
1005 "SUFFIX_ACRONYMS",
1006 "SUFFIX_NOT_ACRONYMS",
1007]