nameparser 2.5.0__tar.gz → 2.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. {nameparser-2.5.0/nameparser.egg-info → nameparser-2.6.0}/PKG-INFO +8 -9
  2. {nameparser-2.5.0 → nameparser-2.6.0}/README.rst +6 -6
  3. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_config_shim.py +11 -2
  4. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_facade.py +5 -4
  5. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_lexicon.py +24 -14
  6. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_parser.py +2 -2
  7. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_pipeline/_assemble.py +2 -2
  8. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_pipeline/_assign.py +27 -21
  9. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_pipeline/_classify.py +75 -14
  10. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_pipeline/_comma.py +99 -34
  11. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_pipeline/_extract.py +13 -10
  12. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_pipeline/_group.py +56 -204
  13. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_pipeline/_pieces.py +59 -37
  14. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_pipeline/_post_rules.py +116 -99
  15. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_pipeline/_script_segment.py +22 -87
  16. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_pipeline/_segment.py +16 -5
  17. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_pipeline/_state.py +6 -5
  18. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_pipeline/_tokenize.py +25 -13
  19. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_pipeline/_vocab.py +97 -27
  20. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_policy.py +49 -14
  21. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_render.py +94 -35
  22. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_types.py +3 -3
  23. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_version.py +4 -4
  24. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/config/__init__.py +2 -0
  25. nameparser-2.6.0/nameparser/config/_compiled.py +2159 -0
  26. nameparser-2.6.0/nameparser/config/_invariants.py +39 -0
  27. nameparser-2.6.0/nameparser/config/bound_given_names.py +27 -0
  28. nameparser-2.6.0/nameparser/config/capitalization.py +45 -0
  29. nameparser-2.6.0/nameparser/config/conjunctions.py +26 -0
  30. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/config/maiden_markers.py +6 -23
  31. nameparser-2.6.0/nameparser/config/particles.py +101 -0
  32. nameparser-2.6.0/nameparser/config/suffixes.py +144 -0
  33. nameparser-2.6.0/nameparser/config/titles.py +60 -0
  34. nameparser-2.6.0/nameparser/vocab/ak.toml +5 -0
  35. nameparser-2.6.0/nameparser/vocab/am.toml +9 -0
  36. nameparser-2.6.0/nameparser/vocab/ar.toml +526 -0
  37. nameparser-2.6.0/nameparser/vocab/as.toml +19 -0
  38. nameparser-2.6.0/nameparser/vocab/be.toml +23 -0
  39. nameparser-2.6.0/nameparser/vocab/bg.toml +61 -0
  40. nameparser-2.6.0/nameparser/vocab/bin.toml +9 -0
  41. nameparser-2.6.0/nameparser/vocab/bn.toml +182 -0
  42. nameparser-2.6.0/nameparser/vocab/bo.toml +13 -0
  43. nameparser-2.6.0/nameparser/vocab/bs.toml +29 -0
  44. nameparser-2.6.0/nameparser/vocab/ca.toml +8 -0
  45. nameparser-2.6.0/nameparser/vocab/cs.toml +51 -0
  46. nameparser-2.6.0/nameparser/vocab/da.toml +41 -0
  47. nameparser-2.6.0/nameparser/vocab/de.toml +207 -0
  48. nameparser-2.6.0/nameparser/vocab/el.toml +33 -0
  49. nameparser-2.6.0/nameparser/vocab/en.toml +4955 -0
  50. nameparser-2.6.0/nameparser/vocab/es.toml +101 -0
  51. nameparser-2.6.0/nameparser/vocab/et.toml +18 -0
  52. nameparser-2.6.0/nameparser/vocab/fa.toml +37 -0
  53. nameparser-2.6.0/nameparser/vocab/ff.toml +5 -0
  54. nameparser-2.6.0/nameparser/vocab/fi.toml +26 -0
  55. nameparser-2.6.0/nameparser/vocab/fil.toml +20 -0
  56. nameparser-2.6.0/nameparser/vocab/fr.toml +183 -0
  57. nameparser-2.6.0/nameparser/vocab/ga.toml +63 -0
  58. nameparser-2.6.0/nameparser/vocab/gd.toml +9 -0
  59. nameparser-2.6.0/nameparser/vocab/gu.toml +13 -0
  60. nameparser-2.6.0/nameparser/vocab/ha.toml +17 -0
  61. nameparser-2.6.0/nameparser/vocab/he.toml +133 -0
  62. nameparser-2.6.0/nameparser/vocab/hi.toml +211 -0
  63. nameparser-2.6.0/nameparser/vocab/hr.toml +41 -0
  64. nameparser-2.6.0/nameparser/vocab/hu.toml +17 -0
  65. nameparser-2.6.0/nameparser/vocab/hy.toml +23 -0
  66. nameparser-2.6.0/nameparser/vocab/id.toml +5 -0
  67. nameparser-2.6.0/nameparser/vocab/is.toml +25 -0
  68. nameparser-2.6.0/nameparser/vocab/it.toml +194 -0
  69. nameparser-2.6.0/nameparser/vocab/ja.toml +144 -0
  70. nameparser-2.6.0/nameparser/vocab/ka.toml +21 -0
  71. nameparser-2.6.0/nameparser/vocab/kk.toml +7 -0
  72. nameparser-2.6.0/nameparser/vocab/km.toml +11 -0
  73. nameparser-2.6.0/nameparser/vocab/kn.toml +5 -0
  74. nameparser-2.6.0/nameparser/vocab/ko.toml +88 -0
  75. nameparser-2.6.0/nameparser/vocab/kok.toml +7 -0
  76. nameparser-2.6.0/nameparser/vocab/lo.toml +5 -0
  77. nameparser-2.6.0/nameparser/vocab/lt.toml +35 -0
  78. nameparser-2.6.0/nameparser/vocab/lv.toml +17 -0
  79. nameparser-2.6.0/nameparser/vocab/mi.toml +5 -0
  80. nameparser-2.6.0/nameparser/vocab/mk.toml +15 -0
  81. nameparser-2.6.0/nameparser/vocab/ml.toml +9 -0
  82. nameparser-2.6.0/nameparser/vocab/mn.toml +11 -0
  83. nameparser-2.6.0/nameparser/vocab/mr.toml +91 -0
  84. nameparser-2.6.0/nameparser/vocab/ms.toml +52 -0
  85. nameparser-2.6.0/nameparser/vocab/mul.toml +58 -0
  86. nameparser-2.6.0/nameparser/vocab/my.toml +5 -0
  87. nameparser-2.6.0/nameparser/vocab/nb.toml +45 -0
  88. nameparser-2.6.0/nameparser/vocab/ne.toml +11 -0
  89. nameparser-2.6.0/nameparser/vocab/nl.toml +147 -0
  90. nameparser-2.6.0/nameparser/vocab/nn.toml +5 -0
  91. nameparser-2.6.0/nameparser/vocab/or.toml +9 -0
  92. nameparser-2.6.0/nameparser/vocab/pa.toml +29 -0
  93. nameparser-2.6.0/nameparser/vocab/pl.toml +24 -0
  94. nameparser-2.6.0/nameparser/vocab/pt.toml +70 -0
  95. nameparser-2.6.0/nameparser/vocab/ro.toml +17 -0
  96. nameparser-2.6.0/nameparser/vocab/ru.toml +88 -0
  97. nameparser-2.6.0/nameparser/vocab/sd.toml +21 -0
  98. nameparser-2.6.0/nameparser/vocab/shn.toml +5 -0
  99. nameparser-2.6.0/nameparser/vocab/si.toml +5 -0
  100. nameparser-2.6.0/nameparser/vocab/sk.toml +29 -0
  101. nameparser-2.6.0/nameparser/vocab/sl.toml +29 -0
  102. nameparser-2.6.0/nameparser/vocab/sr.toml +52 -0
  103. nameparser-2.6.0/nameparser/vocab/sv.toml +38 -0
  104. nameparser-2.6.0/nameparser/vocab/sw.toml +7 -0
  105. nameparser-2.6.0/nameparser/vocab/ta.toml +17 -0
  106. nameparser-2.6.0/nameparser/vocab/te.toml +9 -0
  107. nameparser-2.6.0/nameparser/vocab/th.toml +9 -0
  108. nameparser-2.6.0/nameparser/vocab/ti.toml +9 -0
  109. nameparser-2.6.0/nameparser/vocab/tr.toml +26 -0
  110. nameparser-2.6.0/nameparser/vocab/uk.toml +54 -0
  111. nameparser-2.6.0/nameparser/vocab/und.toml +21 -0
  112. nameparser-2.6.0/nameparser/vocab/ur.toml +33 -0
  113. nameparser-2.6.0/nameparser/vocab/vi.toml +9 -0
  114. nameparser-2.6.0/nameparser/vocab/yi.toml +5 -0
  115. nameparser-2.6.0/nameparser/vocab/yo.toml +15 -0
  116. nameparser-2.6.0/nameparser/vocab/zh.toml +153 -0
  117. nameparser-2.6.0/nameparser/vocab/zu.toml +6 -0
  118. {nameparser-2.5.0 → nameparser-2.6.0/nameparser.egg-info}/PKG-INFO +8 -9
  119. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser.egg-info/SOURCES.txt +93 -0
  120. {nameparser-2.5.0 → nameparser-2.6.0}/pyproject.toml +16 -4
  121. {nameparser-2.5.0 → nameparser-2.6.0}/tests/conftest.py +28 -0
  122. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_capitalization.py +3 -3
  123. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_initials.py +1 -1
  124. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_particles.py +3 -2
  125. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_suffixes.py +1 -1
  126. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/_differential_fixtures.py +32 -1
  127. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/cases.py +1101 -107
  128. nameparser-2.6.0/tests/v2/design_docs.py +498 -0
  129. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/pipeline/test_assemble.py +1 -1
  130. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/pipeline/test_classify.py +18 -7
  131. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/pipeline/test_group.py +2 -2
  132. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/pipeline/test_pieces.py +23 -16
  133. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/pipeline/test_post_rules.py +44 -1
  134. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/pipeline/test_script_segment.py +3 -1
  135. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/pipeline/test_tokenize.py +43 -0
  136. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/pipeline/test_vocab.py +38 -4
  137. nameparser-2.6.0/tests/v2/rules_budget.py +154 -0
  138. nameparser-2.6.0/tests/v2/rules_doc.py +365 -0
  139. nameparser-2.6.0/tests/v2/rules_skeleton.py +482 -0
  140. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_benchmark.py +15 -6
  141. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_cases.py +58 -5
  142. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_config_shim.py +19 -1
  143. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_contracts.py +15 -11
  144. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_corpora.py +8 -8
  145. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_corpus_build.py +12 -0
  146. nameparser-2.6.0/tests/v2/test_design_docs.py +556 -0
  147. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_differential.py +20 -6
  148. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_doc_citations.py +272 -52
  149. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_facade.py +5 -5
  150. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_facade_cases.py +41 -1
  151. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_lexicon.py +69 -6
  152. nameparser-2.6.0/tests/v2/test_local_agreement.py +300 -0
  153. nameparser-2.6.0/tests/v2/test_local_corpora.py +4106 -0
  154. nameparser-2.6.0/tests/v2/test_local_jrc.py +548 -0
  155. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_locales.py +8 -6
  156. nameparser-2.6.0/tests/v2/test_name_usage_tool.py +2295 -0
  157. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_parser.py +6 -4
  158. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_policy.py +4 -1
  159. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_properties.py +144 -15
  160. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_regex_sync.py +3 -1
  161. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_render.py +144 -11
  162. nameparser-2.6.0/tests/v2/test_rules_budget.py +182 -0
  163. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_rules_doc.py +111 -1
  164. nameparser-2.6.0/tests/v2/test_rules_doc_grammar.py +236 -0
  165. nameparser-2.6.0/tests/v2/test_rules_skeleton.py +489 -0
  166. nameparser-2.6.0/tests/v2/test_snapshot.py +150 -0
  167. nameparser-2.6.0/tests/v2/test_vocab.py +885 -0
  168. nameparser-2.5.0/nameparser/config/_invariants.py +0 -53
  169. nameparser-2.5.0/nameparser/config/bound_given_names.py +0 -60
  170. nameparser-2.5.0/nameparser/config/capitalization.py +0 -76
  171. nameparser-2.5.0/nameparser/config/conjunctions.py +0 -94
  172. nameparser-2.5.0/nameparser/config/particles.py +0 -333
  173. nameparser-2.5.0/nameparser/config/suffixes.py +0 -1134
  174. nameparser-2.5.0/nameparser/config/titles.py +0 -1033
  175. nameparser-2.5.0/tests/v2/design_docs.py +0 -107
  176. nameparser-2.5.0/tests/v2/rules_doc.py +0 -267
  177. nameparser-2.5.0/tests/v2/test_design_docs.py +0 -150
  178. nameparser-2.5.0/tests/v2/test_name_usage_tool.py +0 -1156
  179. nameparser-2.5.0/tests/v2/test_rules_doc_grammar.py +0 -168
  180. nameparser-2.5.0/tests/v2/test_snapshot.py +0 -48
  181. {nameparser-2.5.0 → nameparser-2.6.0}/AUTHORS +0 -0
  182. {nameparser-2.5.0 → nameparser-2.6.0}/LICENSE +0 -0
  183. {nameparser-2.5.0 → nameparser-2.6.0}/MANIFEST.in +0 -0
  184. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/__init__.py +0 -0
  185. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/__main__.py +0 -0
  186. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_locale.py +0 -0
  187. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/_pipeline/__init__.py +0 -0
  188. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/config/_deprecated.py +0 -0
  189. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/config/bound_first_names.py +0 -0
  190. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/config/prefixes.py +0 -0
  191. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/config/regexes.py +0 -0
  192. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/config/surnames.py +0 -0
  193. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/locales/__init__.py +0 -0
  194. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/locales/ja.py +0 -0
  195. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/locales/ru.py +0 -0
  196. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/locales/tr_az.py +0 -0
  197. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/locales/zh.py +0 -0
  198. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/parser.py +0 -0
  199. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/py.typed +0 -0
  200. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser/util.py +0 -0
  201. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser.egg-info/dependency_links.txt +0 -0
  202. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser.egg-info/requires.txt +0 -0
  203. {nameparser-2.5.0 → nameparser-2.6.0}/nameparser.egg-info/top_level.txt +0 -0
  204. {nameparser-2.5.0 → nameparser-2.6.0}/setup.cfg +0 -0
  205. {nameparser-2.5.0 → nameparser-2.6.0}/tests/__init__.py +0 -0
  206. {nameparser-2.5.0 → nameparser-2.6.0}/tests/base.py +0 -0
  207. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_bound_given_names.py +0 -0
  208. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_brute_force.py +0 -0
  209. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_comma_variants.py +0 -0
  210. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_conjunctions.py +0 -0
  211. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_constants.py +0 -0
  212. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_east_slavic_patronymic_order.py +0 -0
  213. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_first_name.py +0 -0
  214. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_middle_name_as_last.py +0 -0
  215. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_nicknames.py +0 -0
  216. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_output_format.py +0 -0
  217. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_python_api.py +0 -0
  218. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_titles.py +0 -0
  219. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_turkic_patronymic_order.py +0 -0
  220. {nameparser-2.5.0 → nameparser-2.6.0}/tests/test_variations.py +0 -0
  221. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/__init__.py +0 -0
  222. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/conftest.py +0 -0
  223. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/pipeline/__init__.py +0 -0
  224. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/pipeline/test_assign.py +0 -0
  225. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/pipeline/test_comma.py +0 -0
  226. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/pipeline/test_extract.py +0 -0
  227. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/pipeline/test_segment.py +0 -0
  228. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/pipeline/test_state.py +0 -0
  229. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_cldr_agreement.py +0 -0
  230. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_cldr_corpus.py +0 -0
  231. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_cli.py +0 -0
  232. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_config_aliases.py +0 -0
  233. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_doc_spellings.py +0 -0
  234. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_gnd_corpus.py +0 -0
  235. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_layering.py +0 -0
  236. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_locale.py +0 -0
  237. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_order_correspondence.py +0 -0
  238. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_reprs.py +0 -0
  239. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_types.py +0 -0
  240. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_wikidata_corpus.py +0 -0
  241. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_wikidata_native_corpus.py +0 -0
  242. {nameparser-2.5.0 → nameparser-2.6.0}/tests/v2/test_zh_surnames_tool.py +0 -0
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nameparser
3
- Version: 2.5.0
3
+ Version: 2.6.0
4
4
  Summary: A simple Python module for parsing human names into their individual components.
5
5
  Author-email: Derek Gulbranson <derek73@gmail.com>
6
- License: LGPL
6
+ License-Expression: LGPL-2.1-or-later
7
7
  Keywords: names,parser
8
8
  Classifier: Intended Audience :: Developers
9
- Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)
10
9
  Classifier: Operating System :: OS Independent
11
10
  Classifier: Programming Language :: Python
12
11
  Classifier: Programming Language :: Python :: 3
@@ -36,12 +35,12 @@ nameparser parses human names into seven fields — title, given, middle,
36
35
  family, suffix, nickname, maiden. Results are immutable, configuration is
37
36
  composable, and locale packs are opt-in.
38
37
 
39
- 📣 **nameparser 2.5 is out.** It reads junior and senior written in 45
40
- languages (``Antonio Pavlović mlađi``), keeps a title written before a
41
- family comma out of the surname (``Dr. Smith, John``), reads Chinese and
42
- Japanese credential and office words (``田中社長``), and gives a lone first
43
- name followed by V, I or X its last name back (``Malcolm X``). The `release
44
- log <https://nameparser.readthedocs.io/en/latest/release_log.html>`__ has
38
+ 📣 **nameparser 2.6 is out.** It keeps surnames that earlier releases
39
+ read as credentials (``Luo Si``, ``Maria de Sa``, ``Ram Bahadur B.K.``),
40
+ reads ``Wm`` and ``Md`` as the given names they abbreviate, recognizes more credentials after a comma
41
+ (``John Smith, MBChB``), and adds an opt-in reading for names with two
42
+ family names (``Gabriel García Márquez``). The `release log
43
+ <https://nameparser.readthedocs.io/en/latest/release_log.html>`__ has
45
44
  the details.
46
45
 
47
46
  Chinese, Japanese and Korean names written in their own scripts are read
@@ -7,12 +7,12 @@ nameparser parses human names into seven fields — title, given, middle,
7
7
  family, suffix, nickname, maiden. Results are immutable, configuration is
8
8
  composable, and locale packs are opt-in.
9
9
 
10
- 📣 **nameparser 2.5 is out.** It reads junior and senior written in 45
11
- languages (``Antonio Pavlović mlađi``), keeps a title written before a
12
- family comma out of the surname (``Dr. Smith, John``), reads Chinese and
13
- Japanese credential and office words (``田中社長``), and gives a lone first
14
- name followed by V, I or X its last name back (``Malcolm X``). The `release
15
- log <https://nameparser.readthedocs.io/en/latest/release_log.html>`__ has
10
+ 📣 **nameparser 2.6 is out.** It keeps surnames that earlier releases
11
+ read as credentials (``Luo Si``, ``Maria de Sa``, ``Ram Bahadur B.K.``),
12
+ reads ``Wm`` and ``Md`` as the given names they abbreviate, recognizes more credentials after a comma
13
+ (``John Smith, MBChB``), and adds an opt-in reading for names with two
14
+ family names (``Gabriel García Márquez``). The `release log
15
+ <https://nameparser.readthedocs.io/en/latest/release_log.html>`__ has
16
16
  the details.
17
17
 
18
18
  Chinese, Japanese and Korean names written in their own scripts are read
@@ -1048,6 +1048,7 @@ class Constants:
1048
1048
  from nameparser.config.maiden_markers import MAIDEN_MARKERS
1049
1049
  from nameparser.config.suffixes import GLUED_HONORIFICS
1050
1050
  from nameparser.config.surnames import KOREAN_SURNAMES
1051
+ from nameparser.config.titles import AFTER_GIVEN_TITLES
1051
1052
  # Every set field and every capitalization_exceptions key passes
1052
1053
  # _v1_matchable BEFORE any set algebra below: an entry v1 could
1053
1054
  # never match, or one Lexicon folds to empty (#582), is dropped
@@ -1160,8 +1161,8 @@ class Constants:
1160
1161
  particles=particles,
1161
1162
  # complement translation: v1 marks the never-given subset;
1162
1163
  # v2 marks the may-be-given subset. The trailing union keeps
1163
- # a config v1 accepted: particles.py asserts its own data has
1164
- # no word in both NON_GIVEN_NAME_PARTICLES and
1164
+ # a config v1 accepted: the vocabulary build refuses a
1165
+ # shipped word in both NON_GIVEN_NAME_PARTICLES and
1165
1166
  # BOUND_GIVEN_NAMES, but nothing stops a caller adding one at
1166
1167
  # runtime, and v1 then lets the bound rule win (leading "dos
1167
1168
  # Santos Silva" parses first="dos Santos"). Treating such a
@@ -1207,6 +1208,14 @@ class Constants:
1207
1208
  # stops mattering, the same rule ambiguous_acronyms gets
1208
1209
  # against suffix_acronyms above.
1209
1210
  honorific_tails=GLUED_HONORIFICS & suffix_words,
1211
+ # likewise no v1 manager: the salutations ending a given
1212
+ # part are 2.6 behavior (#835), so they ride in the
1213
+ # snapshot only, and the one v1 knob that reaches them is
1214
+ # deleting the title. Lexicon does not check this pair
1215
+ # (classify reads a member only where it is a title too),
1216
+ # so the intersection is kept for `_snapshot() ==
1217
+ # Lexicon.default()` legibility, as conjunctions_ambiguous'.
1218
+ after_given_titles=AFTER_GIVEN_TITLES & titles,
1210
1219
  # TupleManager is dict[str, object] (v1 parity: values were
1211
1220
  # never statically str-typed); every real entry is a str,
1212
1221
  # same assumption _DelimiterManager's sentinel lookup makes.
@@ -700,10 +700,11 @@ class HumanName:
700
700
  if got or not words or not all(self._is_particle(w)
701
701
  for w in words):
702
702
  return got
703
- # rules/R3: "except the particles of a part whose every
704
- # word is one, which are not acting as particles there"
705
- # -- nothing survived
706
- # the filter, so the whole group is particles. The
703
+ # rules/R3: "A part whose every word is a particle initials
704
+ # those particles: they are not acting as particles there"
705
+ # -- the Except bullet this quote opens; nothing survived
706
+ # the filter, so the whole group is particles
707
+ # (decisions/R3). The
707
708
  # facade's twin of the core's
708
709
  # UNJOINED_TAG. NOT pinned against it by the case runners,
709
710
  # which compare the seven role fields only (Case carries
@@ -25,7 +25,7 @@ _VOCAB_FIELDS = (
25
25
  "titles", "given_name_titles", "suffix_acronyms", "suffix_words",
26
26
  "suffix_acronyms_ambiguous", "particles", "particles_ambiguous",
27
27
  "conjunctions", "conjunctions_ambiguous", "bound_given_names",
28
- "maiden_markers", "surnames", "honorific_tails",
28
+ "maiden_markers", "surnames", "honorific_tails", "after_given_titles",
29
29
  )
30
30
 
31
31
  #: (marker, base, why) triples. Each marker QUALIFIES how entries of
@@ -55,21 +55,19 @@ _VOCAB_FIELDS = (
55
55
  #: as a tail would peel a period-gated word off a real name), and
56
56
  #: nothing needs the acronym half: the shipped tails are CJK
57
57
  #: honorifics, which are words.
58
- #: The same relation is asserted a second time in config/suffixes.py,
59
- #: over the raw GLUED_HONORIFICS/SUFFIX_WORDS constants at
60
- #: import. The two are not redundant in the way they look: that one
61
- #: is an `assert`, stripped under `python -O`, while the check here
62
- #: raises unconditionally -- so under -O this is what still holds the
63
- #: SHIPPED vocabulary to the invariant, as it is the only thing that
64
- #: ever held a caller's own.
58
+ #: The shipped vocabulary holds the relation by construction: its
59
+ #: build puts a word in GLUED_HONORIFICS only through an `attached`
60
+ #: property on a word reading (tools/vocab/build.py). The check here
61
+ #: is the only thing that holds a caller's own.
65
62
  #:
66
- #: Two fields are deliberately NOT here and have no check of their own.
63
+ #: Three fields are deliberately NOT here and have no check of their own.
67
64
  #: given_name_titles -- see the note in __post_init__ for why every
68
65
  #: attempt at one rejected working configurations. conjunctions_ambiguous
69
66
  #: -- an orphan is never consulted because both the classify fork and
70
67
  #: its ambiguity emitter require the base entry as well, so an orphan
71
68
  #: decides nothing, and AGENTS.md's invariants rule guards harm, not
72
- #: no-ops.
69
+ #: no-ops. after_given_titles -- the same: classify tags a word from it
70
+ #: only where the word is in titles too, so an orphan is never read.
73
71
  _SUBSET_FIELDS = (
74
72
  ("particles_ambiguous", "particles",
75
73
  "an orphan emits a spurious particle-or-given ambiguity"),
@@ -692,6 +690,15 @@ class Lexicon:
692
690
  #: vocabulary is. Full default list:
693
691
  #: :data:`~nameparser.config.suffixes.GLUED_HONORIFICS`.
694
692
  honorific_tails: frozenset[str] = frozenset()
693
+ #: Salutations that read as a title where one ENDS the given part
694
+ #: after a family comma, written bare: "Doe, Jane Mrs" reads title
695
+ #: "Mrs" where an office or rank word there stays a middle name
696
+ #: ("Doe, John King"), rules/H8. A period-marked title word reads as
697
+ #: a title there whether listed here or not (rules/H5). Entries need
698
+ #: not also be in ``titles``; one that is not is never consulted.
699
+ #: Full default list:
700
+ #: :data:`~nameparser.config.titles.AFTER_GIVEN_TITLES`.
701
+ after_given_titles: frozenset[str] = frozenset()
695
702
  #: Lowercase word -> case mask used by capitalized(): the word's
696
703
  #: own letters and digits, each in the case it takes ("phd" ->
697
704
  #: "PhD"), laid over the word as written, so the one entry
@@ -739,9 +746,10 @@ class Lexicon:
739
746
  # the expensive one -- three working configurations broken
740
747
  # across two attempts. Do not add a third.
741
748
  #
742
- # The v2 form of particles.py's NON_GIVEN_NAME_PARTICLES-disjoint-
743
- # from-BOUND_GIVEN_NAMES assertion. That module guards its own
744
- # data at import; this guards vocabulary a caller supplies.
749
+ # The v2 form of the vocabulary build's check that a bound given
750
+ # name which is also a particle may be given (tools/vocab/build.py,
751
+ # check 9). The build guards the shipped data; this guards
752
+ # vocabulary a caller supplies.
745
753
  contradictory = (
746
754
  self.bound_given_names & self.particles) - self.particles_ambiguous
747
755
  if contradictory:
@@ -947,7 +955,8 @@ def _default_lexicon() -> Lexicon:
947
955
  SUFFIX_WORDS,
948
956
  )
949
957
  from nameparser.config.surnames import KOREAN_SURNAMES
950
- from nameparser.config.titles import GIVEN_NAME_TITLES, TITLES
958
+ from nameparser.config.titles import (
959
+ AFTER_GIVEN_TITLES, GIVEN_NAME_TITLES, TITLES)
951
960
 
952
961
  # every vocabulary constant is a frozenset since #293, so each one
953
962
  # feeds its strictly-typed frozenset[str] field as it stands -- and
@@ -975,6 +984,7 @@ def _default_lexicon() -> Lexicon:
975
984
  maiden_markers=MAIDEN_MARKERS,
976
985
  surnames=KOREAN_SURNAMES,
977
986
  honorific_tails=GLUED_HONORIFICS,
987
+ after_given_titles=AFTER_GIVEN_TITLES,
978
988
  # pass canonical pair-tuples so this strictly-typed call site never
979
989
  # feeds a Mapping to the tuple-annotated field; __post_init__
980
990
  # still tolerates a Mapping at runtime for interactive use
@@ -41,7 +41,7 @@ class Parser:
41
41
  vocabulary DECLINES, so a locale pack's surnames always win where
42
42
  they match; returning None declines in turn and the token stays
43
43
  whole. Two promises narrow when one is supplied (the first is
44
- rules/A1's Accepted clause):
44
+ rules/A1's Accepted bullet "The one exception to totality"):
45
45
  parse-totality gains its one exception -- an exception raised by
46
46
  the segmenter propagates, because a user-supplied callable's own
47
47
  error is a user-code error, not a content error -- and this Parser
@@ -215,7 +215,7 @@ class Parser:
215
215
  dropped = set(state.dropped)
216
216
  # Force the role BEFORE the entry pass: it keys on
217
217
  # Role.SUFFIX, and a bare value's sub-parse reads its words
218
- # as a name ('MD PhD' is a title and a family there), so
218
+ # as a name ('RA PhD' is a title and a family there), so
219
219
  # inside the sub-parse it joined nothing. The sub-parse's
220
220
  # tags are KEPT, "joined" included: a within-piece mark
221
221
  # (the Ph. D. merge) is role-blind and right for every
@@ -3,7 +3,7 @@
3
3
  Consumes: tokens (all roles set), dropped, ambiguities (by index;
4
4
  one carrying ``field_tail`` is worded from its word's final role,
5
5
  #626), and segments, for the part a withdrawn connective report's
6
- letter ends (rules/P3, #721).
6
+ letter ends (rules/P8, #721).
7
7
  Produces: a validated ParsedName -- the constructor re-checks every
8
8
  invariant (span order/bounds, ambiguity subset), so a pipeline bug
9
9
  that would produce an invalid result dies HERE, not in a renderer
@@ -116,7 +116,7 @@ def assemble(state: ParseState) -> ParsedName:
116
116
  and any(t.role in (Role.SUFFIX, Role.TITLE)
117
117
  for t in materialized)):
118
118
  continue
119
- # rules/P3: "unless no name word of its part stands to its
119
+ # rules/P8: "unless no name word of its part stands to its
120
120
  # right" -- nothing after the letter in its segment, or only
121
121
  # suffixes, titles, a maiden clause or a nickname: it connects
122
122
  # nothing, so the report goes (#721). Asked here for the reason
@@ -15,7 +15,7 @@ in the Han/Hiragana/Katakana repertoire the #272 kana license shares
15
15
  across pieces); token/piece tags; Lexicon only through tags already
16
16
  applied by classify (plus the leading-title period rule).
17
17
 
18
- Implements rules H2, H4, H5, H6, N3, O4, O5, S10, W4, and P6 after a family comma,
18
+ Implements rules H2, H4, H5, H6, H7, H8, N3, O4, O5, S10, W4, and P6 after a family comma,
19
19
  of docs/design/rules/,
20
20
  each cited at its code below. Ports v1's assignment loops.
21
21
  NO_COMMA (per name_order):
@@ -78,8 +78,8 @@ group's head: the comma's own decision, #544's anchored members and
78
78
  not counted here. And
79
79
  at the one site that places a LONE name word, GIVEN_OR_FAMILY for the
80
80
  field the convention picked (O5, #449) and TITLE_OR_NAME for the two
81
- shapes where the doubt is whether a word is a title instead (H4,
82
- #491): the peel leaving one title-vocabulary word standing, and a
81
+ shapes where the doubt is whether a word is a title instead (H4 and
82
+ H7, #491): the peel leaving one title-vocabulary word standing, and a
83
83
  joined unit carrying title vocabulary.
84
84
  """
85
85
  from __future__ import annotations
@@ -339,8 +339,8 @@ def _assign_main(seg_idx: int, state: ParseState,
339
339
  # reads that word as the family name" (history: decisions/N3)
340
340
  # -- v1's p_len == 1 counted
341
341
  # the WHOLE segment before any title peeling -- 'Xyz. (Bud) Smith'
342
- # has two pieces, so the title peel wins and Smith stays the given
343
- # name (pinned live 2026-07-17)
342
+ # has two pieces, so the title peel wins and this rule declines;
343
+ # H1 then names Smith the family (rules/N3's title bullet)
344
344
  # The nickname scan sits LAST in the test: it is a generator, which
345
345
  # every interpreter resumes once per token (seven call events on
346
346
  # the reference name), and only a one-piece segment ever reads its
@@ -478,18 +478,20 @@ def _assign_main(seg_idx: int, state: ParseState,
478
478
  # Jr.' and "'Smitty' Jones Jr." ARE this convention -- and the
479
479
  # count comes off the peel rather than off name_pieces, which
480
480
  # is what keeps the carve-out above out of these branches.
481
- # Title vocabulary in the unit makes the doubt H4's (is the
482
- # WORD a title), anything else O5's (which FIELD it took), so
483
- # only the second reads `field_undecided`.
481
+ # Title vocabulary in the unit makes the doubt H4's or H7's (is
482
+ # the WORD a title), anything else O5's (which FIELD it took),
483
+ # so only the second reads `field_undecided`.
484
484
  if peeled.names == 1 and not resolved.by_script:
485
485
  # rules/H4: "an input whose only remaining name word
486
486
  # after the title peel is itself title vocabulary reads
487
487
  # that word as the name by convention and reports
488
- # `title-or-name`" (history: decisions/H4), and H4's
489
- # join clause, stated at rules/O5 as its exception --
490
- # one branch, its detail naming a field only in the join
491
- # shape ('John of Prince', 'Attorney General of
492
- # Minnesota'), where the unit is more than the title word.
488
+ # `title-or-name`" (history: decisions/H4), and
489
+ # rules/H7: "one name UNIT of more than one word carrying
490
+ # title vocabulary reports `title-or-name`" (history:
491
+ # decisions/H7, decisions/H4 #518) -- one branch, its detail
492
+ # naming a field only in H7's shape ('John of Prince',
493
+ # 'Attorney General of Minnesota'), where the unit is more
494
+ # than the title word.
493
495
  # A LONE title word never reaches here ('Dr.', 'Prince of
494
496
  # Wales'): the leading-title peel took the whole name.
495
497
  if any("vocab:title" in tokens[i].tags for i in head):
@@ -635,7 +637,7 @@ def _inside_a_credential_run(seg: Sequence[Sequence[int]],
635
637
  tokens: list[WorkToken], given_at: int,
636
638
  k: int, end: int,
637
639
  ambiguities: list[PendingAmbiguity]) -> bool:
638
- """rules/P6's third exception for the run `seg[k:end]`: a
640
+ """rules/P9 (P6's third exception) for the run `seg[k:end]`: a
639
641
  run assign read as post-nominals, with a credential read in
640
642
  front of it and another behind, stands INSIDE the credential run
641
643
  assign read whole rather than ending the name, so it keeps that
@@ -1257,7 +1259,11 @@ def assign(state: ParseState) -> ParseState:
1257
1259
  candidates = [k for k in range(n, len(pieces))
1258
1260
  if k == n
1259
1261
  or not reads_as_a_suffix(k, frozenset())]
1260
- kept = trailing_titles(candidates, pieces, ptags, tokens)
1262
+ # rules/H8: "the word joins the title field, and the given
1263
+ # part reads as it would without it" -- a bare salutation
1264
+ # chains here as a period-marked title does
1265
+ kept = trailing_titles(candidates, pieces, ptags, tokens,
1266
+ salutations=True)
1261
1267
  walkable = frozenset(candidates)
1262
1268
  titled_idx = frozenset(candidates[kept:])
1263
1269
  for k in titled_idx:
@@ -1459,8 +1465,8 @@ def assign(state: ParseState) -> ParseState:
1459
1465
  # Berg'), or where everything behind it is post-nominal, which
1460
1466
  # leaves no family word to address -- H3's concern, by a
1461
1467
  # stricter test than H3's one-word give-back, so this is not
1462
- # `leading_titles` ('MD PhD Jr., John' keeps family 'MD', where
1463
- # 'MD PhD Jr. John' reads title 'MD'). A dual is a title wherever
1468
+ # `leading_titles` ('RA PhD Jr., John' keeps family 'RA', where
1469
+ # 'RA PhD Jr. John' reads title 'RA'). A dual is a title wherever
1464
1470
  # a name word follows it ('Rev. Sr. Smith, Mary').
1465
1471
  # `is_title_piece` is H5's vocabulary read, so the two slots that
1466
1472
  # refuse the shape agree on what a title word is.
@@ -1487,10 +1493,10 @@ def assign(state: ParseState) -> ParseState:
1487
1493
  and "particle" not in tokens[fam_pieces[lead][0]].tags):
1488
1494
  _set_roles(tokens, fam_pieces[lead], Role.TITLE)
1489
1495
  lead += 1
1490
- # rules/P2: a particle "joins the words after it into one
1491
- # name part" -- so a particle that is suffix vocabulary too
1492
- # (vd, mc) with a name piece behind it in this part heads
1493
- # that name, and is not peeled from between two family words
1496
+ # rules/P9: "a particle that is also suffix vocabulary heads
1497
+ # the name word behind it" -- with a name piece behind it in
1498
+ # this part (vd, mc), it heads that name, as P2's join does,
1499
+ # and is not peeled from between two family words
1494
1500
  # (#573: 'SMITH VD MA, JOHN', where the uniform case left the
1495
1501
  # chain stopped before VD; 'SMITH VD JR, JOHN', a suffix word
1496
1502
  # behind it, keeps suffix 'VD JR'). Read here because only
@@ -17,7 +17,11 @@ honorific_tails, which script_segment consumes upstream; and, since
17
17
  which decide whether an UNLISTED dotted or all-caps token joins the
18
18
  ambiguous credential class by SHAPE (#516) -- the all-caps half here
19
19
  only under CapsSuffixes.EVERYWHERE, the default's part after a comma
20
- being tagged by `_comma.decide` when group binds it (#613). is_initial also consults
20
+ being tagged by `_comma.decide` when group binds it (#613); and
21
+ Policy.extra_suffix_delimiters, with the built-in semicolon
22
+ (_vocab.delimiter_cores), so a word after the first comma with a
23
+ delimiter glued to it is tagged as the word without it (rules/C10).
24
+ is_initial also consults
21
25
  the _policy module's _NO_INITIALS constant. It is named apart from the
22
26
  fields above because it is not CONFIGURATION -- no Lexicon or Policy
23
27
  carries it and no caller can change it -- and not because it decides
@@ -26,6 +30,7 @@ nothing: the tags this stage writes do vary by it ('씨.' is not tagged
26
30
 
27
31
  Tags emitted -- stable (API): "particle", "conjunction", "initial";
28
32
  namespaced (unstable): "vocab:title", "vocab:given-title",
33
+ "vocab:after-given-title",
29
34
  "vocab:suffix", "vocab:suffix-word", "vocab:suffix-ambiguous",
30
35
  "vocab:particle-ambiguous", "vocab:bound-given", "vocab:maiden-marker",
31
36
  "vocab:maiden-marker-cont"; and, in a namespace of its own,
@@ -62,7 +67,8 @@ from nameparser._pipeline._state import (
62
67
  )
63
68
  from nameparser._types import AmbiguityKind, Role
64
69
  from nameparser._pipeline._vocab import (
65
- _INITIAL, caps_shape_candidate, composed_len, is_initial, is_one_case,
70
+ BUILTIN_SUFFIX_DELIMITERS, _INITIAL, caps_shape_candidate, composed_len,
71
+ delimiter_cores, glued_stem, is_initial, is_one_case, is_paired_initials,
66
72
  period_joined_vocab, suffix_as_written, tag_marker_runs,
67
73
  )
68
74
  from nameparser._pipeline._pieces import own_words
@@ -71,11 +77,11 @@ from nameparser._pipeline._pieces import own_words
71
77
 
72
78
 
73
79
  # rules/S2: "a trailing word of the suffix vocabulary reads as a
74
- # suffix — generational forms and credential acronyms alike, and an
75
- # ambiguous acronym written with its periods, one after each
76
- # letter or one after each of two or more letter chunks, counts
80
+ # suffix — generational forms and credential acronyms alike" -- and
81
+ # rules/S3: "an ambiguous acronym written with its periods, one after
82
+ # each letter or one after each of two or more letter chunks, counts
77
83
  # unambiguously; a single trailing period is the abbreviation shape
78
- # any word can wear and does not" -- and: "a
84
+ # any word can wear and does not" -- and rules/S4: "a
79
85
  # bare ambiguous acronym is consumed only when the name has words to
80
86
  # spare"
81
87
  def _tags_for(token: WorkToken, n: str, state: ParseState,
@@ -123,6 +129,10 @@ def _tags_for(token: WorkToken, n: str, state: ParseState,
123
129
  tags.add("vocab:title")
124
130
  if n in lex.given_name_titles:
125
131
  tags.add("vocab:given-title")
132
+ # rules/H8: "the word joins the title field" -- tagged only where the
133
+ # word is a title too, so an entry outside `titles` is inert
134
+ if n in lex.after_given_titles and n in lex.titles:
135
+ tags.add("vocab:after-given-title")
126
136
  if suffix_as_written(n, token.text, lex):
127
137
  tags.add("vocab:suffix")
128
138
  if n in lex.suffix_words:
@@ -136,12 +146,12 @@ def _tags_for(token: WorkToken, n: str, state: ParseState,
136
146
  tags.add("particle")
137
147
  if n in lex.particles_ambiguous:
138
148
  tags.add("vocab:particle-ambiguous")
139
- # rules/P3: "a single-letter connective reads as an initial
149
+ # rules/P8: "a single-letter connective reads as an initial
140
150
  # where the writing says so: written as a bare Latin capital in a
141
151
  # name that is not written wholly in one case, or — in a name
142
152
  # written wholly in one case, where nothing says so — where the
143
153
  # letter is one the vocabulary marks as reading both ways"
144
- # (#383/#479; history: decisions/P3)
154
+ # (#383/#479; history: decisions/P8)
145
155
  # one letter as the writer sees it: a decomposed 'й' is two code
146
156
  # points (#665), so non-ASCII text is counted composed, after the
147
157
  # C-level tests have declined everything else
@@ -204,8 +214,8 @@ def _tags_for(token: WorkToken, n: str, state: ParseState,
204
214
  # title as a whole ('Lt.Gov.', and by the ANY rule 'Mr.Smith');
205
215
  # else ANY suffix chunk makes it a suffix ('JD.CPA'). Title wins
206
216
  # (v1's continue). The third and fourth are 2.4's by-shape halves
207
- # (#516; rules/S9: "An unlisted word joins the ambiguous class
208
- # (S4) by SHAPE where the caller asks for it") and reach only a
217
+ # (#516; rules/S9: "joins the ambiguous class (S4) by SHAPE, in
218
+ # the positions each shape is admitted") and reach only a
209
219
  # token NO vocabulary claimed: a dotted token of two or more
210
220
  # alphabetic chunks, and -- where its switch is on -- an unlisted
211
221
  # all-caps word. They are an `elif` chain
@@ -255,7 +265,16 @@ def _tags_for(token: WorkToken, n: str, state: ParseState,
255
265
  # shipped ambiguous vocabulary carries no periods, so
256
266
  # nothing default changes.
257
267
  tags.add(SHAPE_ACRONYM_TAG)
258
- if state.policy.unlisted_dotted_suffixes:
268
+ # rules/S3: "Paired initials are the exception, as C5 defines
269
+ # them, in a script that writes capitals" -- the shape
270
+ # tag alone, as with the switch off, so the trailing slots
271
+ # read them as a name word and report the fork; the comma
272
+ # restores the class tag where C5 reads them
273
+ # (decisions/S3 (#864)). Cased: a text with no case mapping
274
+ # is in a script without capitals ('พ.บ.').
275
+ if (state.policy.unlisted_dotted_suffixes
276
+ and not (is_paired_initials(token.text)
277
+ and token.text.upper() != token.text.lower())):
259
278
  tags.add(AMBIGUOUS_ACRONYM_TAG)
260
279
  elif (state.policy.unlisted_caps_suffixes is CapsSuffixes.EVERYWHERE
261
280
  and token.role is None
@@ -289,6 +308,16 @@ def _tags_for(token: WorkToken, n: str, state: ParseState,
289
308
  return frozenset(tags)
290
309
 
291
310
 
311
+ #: The tags of a word that joins the word after it, which a delimiter
312
+ #: glued to the word stands between (rules/C10). A particle's are among
313
+ #: them though it also attaches backward (rules/P6): kept, it chained
314
+ #: forward across the delimiter ('Berg, de; Smith' read family 'Berg de;
315
+ #: Smith'), and blocking the forward joins alone would guard every site
316
+ #: that reads the tag (decisions/C10).
317
+ _JOINING_TAGS = frozenset({"particle", "conjunction",
318
+ "vocab:particle-ambiguous", "vocab:bound-given"})
319
+
320
+
292
321
  def classify(state: ParseState) -> ParseState:
293
322
  # One fold per token, shared by the marker pass and the vocabulary
294
323
  # tags -- the shape suffix_as_written already asks for ("n is
@@ -325,15 +354,47 @@ def classify(state: ParseState) -> ParseState:
325
354
  whole_one_case = one_case and (
326
355
  len(own) == len(state.tokens)
327
356
  or is_one_case([t.text for t in state.tokens]))
357
+ # rules/C10: "the word keeps the delimiter in its text and reads as
358
+ # it would without it" -- tagged as the word without it ('MD;' as
359
+ # 'MD'), so every reading that asks a word's vocabulary asks the
360
+ # stem's; only where the delimiter "stands after the first comma",
361
+ # a semicolon before it being a word (decisions/C10, D15)
362
+ readers = list(state.tokens)
363
+ stemmed: set[int] = set()
364
+ if state.comma_offsets:
365
+ policy_cores = state.policy.extra_suffix_delimiters
366
+ cores = (BUILTIN_SUFFIX_DELIMITERS if not policy_cores
367
+ else delimiter_cores(policy_cores))
368
+ first = state.comma_offsets[0]
369
+ for i, t in enumerate(state.tokens):
370
+ text = t.text
371
+ if t.span.start < first:
372
+ continue
373
+ for core in cores:
374
+ if core in text:
375
+ stem = glued_stem(text, cores)
376
+ if stem is not None:
377
+ readers[i] = copy_with(t, text=stem)
378
+ folded[i] = _normalize(stem)
379
+ stemmed.add(i)
380
+ break
328
381
  tokens = tuple(
329
382
  copy_with(
330
- t, tags=_tags_for(t, folded[i], state, marker_tags.get(i),
383
+ t, tags=_tags_for(readers[i], folded[i], state, marker_tags.get(i),
331
384
  one_case_own=one_case and i < clause_at
332
385
  and t.role is None, one_case=one_case,
333
386
  letter_one_case=whole_one_case or (
334
387
  one_case and i < clause_at
335
388
  and t.role is None)))
336
389
  for i, t in enumerate(state.tokens))
390
+ # A word with a delimiter glued to it joins no neighbour across the
391
+ # delimiter, so the stem's joining tags stay off it: 'y;' is no
392
+ # connective to the word after it (rules/C10: "no join, maiden
393
+ # clause or connective reaches across it")
394
+ if stemmed:
395
+ tokens = tuple(
396
+ copy_with(t, tags=t.tags - _JOINING_TAGS) if i in stemmed else t
397
+ for i, t in enumerate(tokens))
337
398
  # Delimited content whose vocabulary cannot settle it: extract's
338
399
  # escape sends an UNambiguous suffix straight through ("(MBA)" ->
339
400
  # suffix) and keeps everything else as a nickname, so an AMBIGUOUS
@@ -372,14 +433,14 @@ def classify(state: ParseState) -> ParseState:
372
433
  # hands classify tokens it did not build; no such path exists
373
434
  # today. `conjunctions` is the clause doing real work: it keeps
374
435
  # an orphan marker (a conjunctions_ambiguous entry no longer in
375
- # conjunctions) inert rather than reported (decisions/P3).
436
+ # conjunctions) inert rather than reported (decisions/P8).
376
437
  # `i < clause_at and token.role is None` mirrors the fork's own
377
438
  # "own words" test above, for the same reason (rules/P3): a
378
439
  # clause's words were never eligible for the fork, so they must
379
440
  # never be eligible to report either. Emitted at the decision
380
441
  # site (mechanisms.md#AMBIGUITY-AT-THE-DECISION-SITE), per
381
442
  # token -- 'e and e e' emits twice; assemble withdraws the
382
- # report on a letter no name word follows (rules/P3, #721).
443
+ # report on a letter no name word follows (rules/P8, #721).
383
444
  if (one_case and "initial" in token.tags
384
445
  and folded[i] in state.lexicon.conjunctions_ambiguous
385
446
  and folded[i] in state.lexicon.conjunctions