nameparser 2.5.0__tar.gz → 2.7.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 (248) hide show
  1. {nameparser-2.5.0/nameparser.egg-info → nameparser-2.7.0}/PKG-INFO +11 -9
  2. {nameparser-2.5.0 → nameparser-2.7.0}/README.rst +9 -6
  3. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/__init__.py +5 -1
  4. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_config_shim.py +101 -6
  5. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_facade.py +5 -4
  6. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_lexicon.py +144 -22
  7. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_parser.py +2 -2
  8. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_pipeline/_assemble.py +38 -5
  9. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_pipeline/_assign.py +516 -106
  10. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_pipeline/_classify.py +219 -38
  11. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_pipeline/_comma.py +228 -73
  12. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_pipeline/_extract.py +17 -13
  13. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_pipeline/_group.py +198 -241
  14. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_pipeline/_pieces.py +462 -134
  15. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_pipeline/_post_rules.py +228 -138
  16. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_pipeline/_script_segment.py +22 -87
  17. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_pipeline/_segment.py +17 -6
  18. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_pipeline/_state.py +99 -10
  19. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_pipeline/_tokenize.py +25 -13
  20. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_pipeline/_vocab.py +719 -89
  21. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_policy.py +54 -15
  22. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_render.py +154 -36
  23. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_types.py +191 -32
  24. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_version.py +4 -4
  25. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/config/__init__.py +2 -0
  26. nameparser-2.7.0/nameparser/config/_compiled.py +2573 -0
  27. nameparser-2.7.0/nameparser/config/_invariants.py +39 -0
  28. nameparser-2.7.0/nameparser/config/_titles_2_6_0.py +826 -0
  29. nameparser-2.7.0/nameparser/config/bound_given_names.py +27 -0
  30. nameparser-2.7.0/nameparser/config/capitalization.py +46 -0
  31. nameparser-2.7.0/nameparser/config/conjunctions.py +26 -0
  32. nameparser-2.7.0/nameparser/config/given_names.py +18 -0
  33. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/config/maiden_markers.py +6 -23
  34. nameparser-2.7.0/nameparser/config/particles.py +102 -0
  35. nameparser-2.7.0/nameparser/config/suffixes.py +165 -0
  36. nameparser-2.7.0/nameparser/config/titles.py +99 -0
  37. nameparser-2.7.0/nameparser/vocab/ak.toml +5 -0
  38. nameparser-2.7.0/nameparser/vocab/am.toml +9 -0
  39. nameparser-2.7.0/nameparser/vocab/ar.toml +539 -0
  40. nameparser-2.7.0/nameparser/vocab/as.toml +19 -0
  41. nameparser-2.7.0/nameparser/vocab/be.toml +23 -0
  42. nameparser-2.7.0/nameparser/vocab/bg.toml +61 -0
  43. nameparser-2.7.0/nameparser/vocab/bin.toml +9 -0
  44. nameparser-2.7.0/nameparser/vocab/bn.toml +181 -0
  45. nameparser-2.7.0/nameparser/vocab/bo.toml +13 -0
  46. nameparser-2.7.0/nameparser/vocab/bs.toml +29 -0
  47. nameparser-2.7.0/nameparser/vocab/ca.toml +8 -0
  48. nameparser-2.7.0/nameparser/vocab/cs.toml +49 -0
  49. nameparser-2.7.0/nameparser/vocab/da.toml +41 -0
  50. nameparser-2.7.0/nameparser/vocab/de.toml +212 -0
  51. nameparser-2.7.0/nameparser/vocab/el.toml +33 -0
  52. nameparser-2.7.0/nameparser/vocab/en.toml +5032 -0
  53. nameparser-2.7.0/nameparser/vocab/es.toml +103 -0
  54. nameparser-2.7.0/nameparser/vocab/et.toml +18 -0
  55. nameparser-2.7.0/nameparser/vocab/fa.toml +37 -0
  56. nameparser-2.7.0/nameparser/vocab/ff.toml +5 -0
  57. nameparser-2.7.0/nameparser/vocab/fi.toml +26 -0
  58. nameparser-2.7.0/nameparser/vocab/fil.toml +28 -0
  59. nameparser-2.7.0/nameparser/vocab/fr.toml +183 -0
  60. nameparser-2.7.0/nameparser/vocab/ga.toml +63 -0
  61. nameparser-2.7.0/nameparser/vocab/gd.toml +9 -0
  62. nameparser-2.7.0/nameparser/vocab/gu.toml +13 -0
  63. nameparser-2.7.0/nameparser/vocab/ha.toml +17 -0
  64. nameparser-2.7.0/nameparser/vocab/he.toml +134 -0
  65. nameparser-2.7.0/nameparser/vocab/hi.toml +210 -0
  66. nameparser-2.7.0/nameparser/vocab/hr.toml +41 -0
  67. nameparser-2.7.0/nameparser/vocab/hu.toml +17 -0
  68. nameparser-2.7.0/nameparser/vocab/hy.toml +23 -0
  69. nameparser-2.7.0/nameparser/vocab/id.toml +5 -0
  70. nameparser-2.7.0/nameparser/vocab/is.toml +25 -0
  71. nameparser-2.7.0/nameparser/vocab/it.toml +195 -0
  72. nameparser-2.7.0/nameparser/vocab/ja.toml +144 -0
  73. nameparser-2.7.0/nameparser/vocab/ka.toml +21 -0
  74. nameparser-2.7.0/nameparser/vocab/kk.toml +7 -0
  75. nameparser-2.7.0/nameparser/vocab/km.toml +11 -0
  76. nameparser-2.7.0/nameparser/vocab/kn.toml +5 -0
  77. nameparser-2.7.0/nameparser/vocab/ko.toml +88 -0
  78. nameparser-2.7.0/nameparser/vocab/kok.toml +7 -0
  79. nameparser-2.7.0/nameparser/vocab/lo.toml +5 -0
  80. nameparser-2.7.0/nameparser/vocab/lt.toml +35 -0
  81. nameparser-2.7.0/nameparser/vocab/lv.toml +17 -0
  82. nameparser-2.7.0/nameparser/vocab/mi.toml +5 -0
  83. nameparser-2.7.0/nameparser/vocab/mk.toml +15 -0
  84. nameparser-2.7.0/nameparser/vocab/ml.toml +9 -0
  85. nameparser-2.7.0/nameparser/vocab/mn.toml +11 -0
  86. nameparser-2.7.0/nameparser/vocab/mr.toml +91 -0
  87. nameparser-2.7.0/nameparser/vocab/ms.toml +52 -0
  88. nameparser-2.7.0/nameparser/vocab/mul.toml +58 -0
  89. nameparser-2.7.0/nameparser/vocab/my.toml +5 -0
  90. nameparser-2.7.0/nameparser/vocab/nb.toml +45 -0
  91. nameparser-2.7.0/nameparser/vocab/ne.toml +11 -0
  92. nameparser-2.7.0/nameparser/vocab/nl.toml +150 -0
  93. nameparser-2.7.0/nameparser/vocab/nn.toml +5 -0
  94. nameparser-2.7.0/nameparser/vocab/or.toml +9 -0
  95. nameparser-2.7.0/nameparser/vocab/pa.toml +30 -0
  96. nameparser-2.7.0/nameparser/vocab/pl.toml +25 -0
  97. nameparser-2.7.0/nameparser/vocab/pt-BR.toml +49 -0
  98. nameparser-2.7.0/nameparser/vocab/pt.toml +101 -0
  99. nameparser-2.7.0/nameparser/vocab/ro.toml +17 -0
  100. nameparser-2.7.0/nameparser/vocab/ru.toml +88 -0
  101. nameparser-2.7.0/nameparser/vocab/sd.toml +21 -0
  102. nameparser-2.7.0/nameparser/vocab/shn.toml +5 -0
  103. nameparser-2.7.0/nameparser/vocab/si.toml +5 -0
  104. nameparser-2.7.0/nameparser/vocab/sk.toml +29 -0
  105. nameparser-2.7.0/nameparser/vocab/sl.toml +29 -0
  106. nameparser-2.7.0/nameparser/vocab/sr.toml +51 -0
  107. nameparser-2.7.0/nameparser/vocab/sv.toml +38 -0
  108. nameparser-2.7.0/nameparser/vocab/sw.toml +6 -0
  109. nameparser-2.7.0/nameparser/vocab/ta.toml +17 -0
  110. nameparser-2.7.0/nameparser/vocab/te.toml +9 -0
  111. nameparser-2.7.0/nameparser/vocab/th.toml +9 -0
  112. nameparser-2.7.0/nameparser/vocab/ti.toml +9 -0
  113. nameparser-2.7.0/nameparser/vocab/tr.toml +28 -0
  114. nameparser-2.7.0/nameparser/vocab/uk.toml +54 -0
  115. nameparser-2.7.0/nameparser/vocab/und.toml +21 -0
  116. nameparser-2.7.0/nameparser/vocab/ur.toml +33 -0
  117. nameparser-2.7.0/nameparser/vocab/vi.toml +9 -0
  118. nameparser-2.7.0/nameparser/vocab/yi.toml +5 -0
  119. nameparser-2.7.0/nameparser/vocab/yo.toml +15 -0
  120. nameparser-2.7.0/nameparser/vocab/zh.toml +153 -0
  121. nameparser-2.7.0/nameparser/vocab/zu.toml +6 -0
  122. {nameparser-2.5.0 → nameparser-2.7.0/nameparser.egg-info}/PKG-INFO +11 -9
  123. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser.egg-info/SOURCES.txt +98 -0
  124. {nameparser-2.5.0 → nameparser-2.7.0}/pyproject.toml +16 -4
  125. {nameparser-2.5.0 → nameparser-2.7.0}/tests/conftest.py +28 -0
  126. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_capitalization.py +3 -3
  127. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_conjunctions.py +2 -2
  128. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_first_name.py +11 -2
  129. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_initials.py +1 -1
  130. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_particles.py +3 -2
  131. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_suffixes.py +49 -34
  132. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_titles.py +4 -4
  133. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/_differential_fixtures.py +62 -1
  134. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/cases.py +3072 -511
  135. nameparser-2.7.0/tests/v2/design_docs.py +498 -0
  136. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/pipeline/test_assemble.py +3 -2
  137. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/pipeline/test_assign.py +51 -17
  138. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/pipeline/test_classify.py +20 -9
  139. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/pipeline/test_comma.py +16 -12
  140. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/pipeline/test_group.py +38 -29
  141. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/pipeline/test_pieces.py +67 -26
  142. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/pipeline/test_post_rules.py +57 -7
  143. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/pipeline/test_script_segment.py +17 -12
  144. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/pipeline/test_state.py +7 -6
  145. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/pipeline/test_tokenize.py +43 -0
  146. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/pipeline/test_vocab.py +343 -17
  147. nameparser-2.7.0/tests/v2/rules_budget.py +154 -0
  148. nameparser-2.7.0/tests/v2/rules_doc.py +366 -0
  149. nameparser-2.7.0/tests/v2/rules_skeleton.py +482 -0
  150. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_benchmark.py +77 -8
  151. nameparser-2.7.0/tests/v2/test_cases.py +814 -0
  152. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_config_shim.py +210 -1
  153. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_contracts.py +18 -12
  154. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_corpora.py +8 -8
  155. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_corpus_build.py +12 -0
  156. nameparser-2.7.0/tests/v2/test_design_docs.py +556 -0
  157. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_differential.py +38 -6
  158. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_doc_citations.py +272 -52
  159. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_facade.py +26 -12
  160. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_facade_cases.py +48 -1
  161. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_lexicon.py +91 -14
  162. nameparser-2.7.0/tests/v2/test_local_agreement.py +470 -0
  163. nameparser-2.7.0/tests/v2/test_local_corpora.py +4498 -0
  164. nameparser-2.7.0/tests/v2/test_local_jrc.py +548 -0
  165. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_locales.py +12 -9
  166. nameparser-2.7.0/tests/v2/test_measure.py +178 -0
  167. nameparser-2.7.0/tests/v2/test_name_usage_tool.py +2297 -0
  168. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_parser.py +162 -38
  169. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_policy.py +5 -2
  170. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_properties.py +434 -33
  171. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_regex_sync.py +27 -3
  172. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_render.py +197 -11
  173. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_reprs.py +6 -1
  174. nameparser-2.7.0/tests/v2/test_rules_budget.py +182 -0
  175. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_rules_doc.py +111 -1
  176. nameparser-2.7.0/tests/v2/test_rules_doc_grammar.py +236 -0
  177. nameparser-2.7.0/tests/v2/test_rules_skeleton.py +489 -0
  178. nameparser-2.7.0/tests/v2/test_snapshot.py +224 -0
  179. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_types.py +140 -5
  180. nameparser-2.7.0/tests/v2/test_vocab.py +1138 -0
  181. nameparser-2.7.0/tests/v2/test_word_report.py +309 -0
  182. nameparser-2.5.0/nameparser/config/_invariants.py +0 -53
  183. nameparser-2.5.0/nameparser/config/bound_given_names.py +0 -60
  184. nameparser-2.5.0/nameparser/config/capitalization.py +0 -76
  185. nameparser-2.5.0/nameparser/config/conjunctions.py +0 -94
  186. nameparser-2.5.0/nameparser/config/particles.py +0 -333
  187. nameparser-2.5.0/nameparser/config/suffixes.py +0 -1134
  188. nameparser-2.5.0/nameparser/config/titles.py +0 -1033
  189. nameparser-2.5.0/tests/v2/design_docs.py +0 -107
  190. nameparser-2.5.0/tests/v2/rules_doc.py +0 -267
  191. nameparser-2.5.0/tests/v2/test_cases.py +0 -362
  192. nameparser-2.5.0/tests/v2/test_design_docs.py +0 -150
  193. nameparser-2.5.0/tests/v2/test_name_usage_tool.py +0 -1156
  194. nameparser-2.5.0/tests/v2/test_rules_doc_grammar.py +0 -168
  195. nameparser-2.5.0/tests/v2/test_snapshot.py +0 -48
  196. {nameparser-2.5.0 → nameparser-2.7.0}/AUTHORS +0 -0
  197. {nameparser-2.5.0 → nameparser-2.7.0}/LICENSE +0 -0
  198. {nameparser-2.5.0 → nameparser-2.7.0}/MANIFEST.in +0 -0
  199. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/__main__.py +0 -0
  200. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_locale.py +0 -0
  201. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/_pipeline/__init__.py +0 -0
  202. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/config/_deprecated.py +0 -0
  203. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/config/bound_first_names.py +0 -0
  204. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/config/prefixes.py +0 -0
  205. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/config/regexes.py +0 -0
  206. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/config/surnames.py +0 -0
  207. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/locales/__init__.py +0 -0
  208. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/locales/ja.py +0 -0
  209. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/locales/ru.py +0 -0
  210. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/locales/tr_az.py +0 -0
  211. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/locales/zh.py +0 -0
  212. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/parser.py +0 -0
  213. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/py.typed +0 -0
  214. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser/util.py +0 -0
  215. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser.egg-info/dependency_links.txt +0 -0
  216. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser.egg-info/requires.txt +0 -0
  217. {nameparser-2.5.0 → nameparser-2.7.0}/nameparser.egg-info/top_level.txt +0 -0
  218. {nameparser-2.5.0 → nameparser-2.7.0}/setup.cfg +0 -0
  219. {nameparser-2.5.0 → nameparser-2.7.0}/tests/__init__.py +0 -0
  220. {nameparser-2.5.0 → nameparser-2.7.0}/tests/base.py +0 -0
  221. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_bound_given_names.py +0 -0
  222. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_brute_force.py +0 -0
  223. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_comma_variants.py +0 -0
  224. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_constants.py +0 -0
  225. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_east_slavic_patronymic_order.py +0 -0
  226. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_middle_name_as_last.py +0 -0
  227. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_nicknames.py +0 -0
  228. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_output_format.py +0 -0
  229. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_python_api.py +0 -0
  230. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_turkic_patronymic_order.py +0 -0
  231. {nameparser-2.5.0 → nameparser-2.7.0}/tests/test_variations.py +0 -0
  232. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/__init__.py +0 -0
  233. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/conftest.py +0 -0
  234. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/pipeline/__init__.py +0 -0
  235. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/pipeline/test_extract.py +0 -0
  236. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/pipeline/test_segment.py +0 -0
  237. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_cldr_agreement.py +0 -0
  238. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_cldr_corpus.py +0 -0
  239. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_cli.py +0 -0
  240. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_config_aliases.py +0 -0
  241. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_doc_spellings.py +0 -0
  242. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_gnd_corpus.py +0 -0
  243. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_layering.py +0 -0
  244. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_locale.py +0 -0
  245. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_order_correspondence.py +0 -0
  246. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_wikidata_corpus.py +0 -0
  247. {nameparser-2.5.0 → nameparser-2.7.0}/tests/v2/test_wikidata_native_corpus.py +0 -0
  248. {nameparser-2.5.0 → nameparser-2.7.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.7.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,15 @@ 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.7 is out.** It is the first release working towards
39
+ the new language and vocabulary model described in the `vocabulary
40
+ model RFC <https://github.com/derek73/python-nameparser/blob/master/docs/design/vocabulary-model-rfc.md>`__.
41
+ It introduces more nuanced handling of titles and particles
42
+ (``Chief Justice John Roberts`` reads the title ``Chief Justice``,
43
+ while ``Justice Smith`` keeps the given name ``Justice``), adjusts the
44
+ default vocabulary to better align with example name corpora, and
45
+ surfaces alternative name readings in the published API. The `release log
46
+ <https://nameparser.readthedocs.io/en/latest/release_log.html>`__ has
45
47
  the details.
46
48
 
47
49
  Chinese, Japanese and Korean names written in their own scripts are read
@@ -7,12 +7,15 @@ 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.7 is out.** It is the first release working towards
11
+ the new language and vocabulary model described in the `vocabulary
12
+ model RFC <https://github.com/derek73/python-nameparser/blob/master/docs/design/vocabulary-model-rfc.md>`__.
13
+ It introduces more nuanced handling of titles and particles
14
+ (``Chief Justice John Roberts`` reads the title ``Chief Justice``,
15
+ while ``Justice Smith`` keeps the given name ``Justice``), adjusts the
16
+ default vocabulary to better align with example name corpora, and
17
+ surfaces alternative name readings in the published API. The `release log
18
+ <https://nameparser.readthedocs.io/en/latest/release_log.html>`__ has
16
19
  the details.
17
20
 
18
21
  Chinese, Japanese and Korean names written in their own scripts are read
@@ -27,6 +27,9 @@ from nameparser._types import (
27
27
  Ambiguity,
28
28
  AmbiguityKind,
29
29
  ParsedName,
30
+ Reading,
31
+ ReadingKind,
32
+ ReadingSource,
30
33
  Role,
31
34
  Segmentation,
32
35
  Segmenter,
@@ -38,7 +41,8 @@ __all__ = [
38
41
  # v1 (compatibility layer)
39
42
  "HumanName",
40
43
  # v2 core
41
- "Span", "Role", "Token", "Ambiguity", "AmbiguityKind", "ParsedName",
44
+ "Span", "Role", "Token", "Ambiguity", "AmbiguityKind", "Reading",
45
+ "ReadingKind", "ReadingSource", "ParsedName",
42
46
  "STABLE_TAGS", "Segmentation", "Segmenter",
43
47
  "Lexicon", "Policy", "PolicyPatch", "PatronymicRule", "Script", "UNSET",
44
48
  "CapsSuffixes",
@@ -731,13 +731,19 @@ def _default_vocab() -> dict[str, frozenset[str]]:
731
731
  from nameparser.config.suffixes import (
732
732
  SUFFIX_ACRONYMS, SUFFIX_ACRONYMS_AMBIGUOUS, SUFFIX_WORDS,
733
733
  )
734
- from nameparser.config.titles import GIVEN_NAME_TITLES, TITLES
734
+ from nameparser.config._titles_2_6_0 import TITLES_2_6_0
735
+ from nameparser.config.titles import (
736
+ GIVEN_NAME_TITLES, TITLES, TITLES_AMBIGUOUS)
735
737
  return {
736
738
  "prefixes": PARTICLES,
737
739
  "suffix_acronyms": SUFFIX_ACRONYMS,
738
740
  "suffix_not_acronyms": SUFFIX_WORDS,
739
741
  "suffix_acronyms_ambiguous": SUFFIX_ACRONYMS_AMBIGUOUS,
740
- "titles": TITLES,
742
+ # 2.6.0's titles, less any word that is no title now: one a
743
+ # later release made a title only inside a title phrase
744
+ # (rules/H9) stays a v1 title, so remove() and `in` answer as
745
+ # they did; _build_snapshot reads it by the phrase
746
+ "titles": TITLES | (TITLES_2_6_0 & TITLES_AMBIGUOUS),
741
747
  "first_name_titles": GIVEN_NAME_TITLES,
742
748
  "conjunctions": CONJUNCTIONS,
743
749
  "bound_first_names": BOUND_GIVEN_NAMES,
@@ -1045,9 +1051,15 @@ class Constants:
1045
1051
  removal path.
1046
1052
  """
1047
1053
  from nameparser.config.conjunctions import CONJUNCTIONS_AMBIGUOUS
1054
+ from nameparser.config.given_names import GIVEN_NAME_ABBREVIATIONS
1048
1055
  from nameparser.config.maiden_markers import MAIDEN_MARKERS
1049
- from nameparser.config.suffixes import GLUED_HONORIFICS
1056
+ from nameparser.config.suffixes import (
1057
+ AGNOMENS, GLUED_HONORIFICS, SUFFIX_WORDS_AMBIGUOUS)
1050
1058
  from nameparser.config.surnames import KOREAN_SURNAMES
1059
+ from nameparser.config._titles_2_6_0 import TITLES_2_6_0
1060
+ from nameparser.config.titles import (
1061
+ AFTER_GIVEN_TITLES, TITLE_MODIFIERS, TITLE_PHRASES, TITLE_STYLES,
1062
+ TITLES_AMBIGUOUS)
1051
1063
  # Every set field and every capitalization_exceptions key passes
1052
1064
  # _v1_matchable BEFORE any set algebra below: an entry v1 could
1053
1065
  # never match, or one Lexicon folds to empty (#582), is dropped
@@ -1070,6 +1082,27 @@ class Constants:
1070
1082
  conjunctions = matchable("conjunctions")
1071
1083
  bound = matchable("bound_first_names")
1072
1084
  titles = matchable("titles")
1085
+ # A title 2.6.0 listed that a later release made a title only
1086
+ # inside a title phrase (rules/H9) is in v1's default `titles`,
1087
+ # where 2.6.0 had it, so remove(), discard() and `in` answer as
1088
+ # they did (decisions/H9, 2026-10-26). Kept, it is read as the
1089
+ # default reads it, by the phrase; removed, it is no title at
1090
+ # all, as removing it made it in 2.6.0. The one 2.6.0 reading
1091
+ # with no v1 spelling left is the word as a title everywhere:
1092
+ # v1 can say "a title" or "not a title", and "a title" now means
1093
+ # the phrase reading. A word 2.6.0 never listed (`dean`) is not
1094
+ # in `moved`, so adding it is still a title everywhere, the 1.4
1095
+ # recipe the phrase test honours below. Compared folded, as the
1096
+ # two cross-set computations below are (#582), so a spelling
1097
+ # Lexicon folds onto a moved word (`king。`) is read as it is.
1098
+ moved = TITLES_2_6_0 & TITLES_AMBIGUOUS
1099
+ kept_moved = frozenset(w for w in titles if _normalize(w) in moved)
1100
+ phrase_only = ((TITLES_AMBIGUOUS - moved) | kept_moved
1101
+ if titles else frozenset())
1102
+ # ...and the three intersections with the phrase vocabulary read
1103
+ # the same folded spellings, or a folded moved word kept its
1104
+ # phrase reading but lost its modifier one (`wing。`)
1105
+ any_title = frozenset(_normalize(w) for w in titles | phrase_only)
1073
1106
  first_name_titles = matchable("first_name_titles")
1074
1107
  non_given = matchable("non_first_name_prefixes")
1075
1108
  ambiguous_acronyms = matchable("suffix_acronyms_ambiguous") & acronyms
@@ -1135,7 +1168,7 @@ class Constants:
1135
1168
  # v1 also had, later, at capitalize().
1136
1169
  try:
1137
1170
  lexicon = Lexicon(
1138
- titles=titles,
1171
+ titles=titles - kept_moved,
1139
1172
  # TRANSLATE, do not filter. The two versions build the same
1140
1173
  # lookup key differently: v1 joins the raw title run and
1141
1174
  # then applies lc(), which strips only the whole string's
@@ -1160,8 +1193,8 @@ class Constants:
1160
1193
  particles=particles,
1161
1194
  # complement translation: v1 marks the never-given subset;
1162
1195
  # 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
1196
+ # a config v1 accepted: the vocabulary build refuses a
1197
+ # shipped word in both NON_GIVEN_NAME_PARTICLES and
1165
1198
  # BOUND_GIVEN_NAMES, but nothing stops a caller adding one at
1166
1199
  # runtime, and v1 then lets the bound rule win (leading "dos
1167
1200
  # Santos Silva" parses first="dos Santos"). Treating such a
@@ -1192,6 +1225,15 @@ class Constants:
1192
1225
  # through this intersection.
1193
1226
  conjunctions_ambiguous=CONJUNCTIONS_AMBIGUOUS & conjunctions,
1194
1227
  bound_given_names=bound,
1228
+ # likewise no v1 manager: an abbreviated given name
1229
+ # reading over rules/H2's shape is 2.7 behavior (#789),
1230
+ # so the set rides in the snapshot only, passed whole.
1231
+ # No v1 knob reaches it, and none needs to: a word a v1
1232
+ # caller ADDS to `titles` is a title wherever it stands,
1233
+ # since the leading peel asks the title vocabulary before
1234
+ # it weighs the shape, as it did in 1.4
1235
+ # (`c.titles.add("chas")`)
1236
+ given_name_abbreviations=GIVEN_NAME_ABBREVIATIONS,
1195
1237
  # v1 Constants has no manager for these (#274 is 2.0
1196
1238
  # behavior); the data module is the only source
1197
1239
  maiden_markers=MAIDEN_MARKERS,
@@ -1207,6 +1249,59 @@ class Constants:
1207
1249
  # stops mattering, the same rule ambiguous_acronyms gets
1208
1250
  # against suffix_acronyms above.
1209
1251
  honorific_tails=GLUED_HONORIFICS & suffix_words,
1252
+ # likewise no v1 manager: the salutations ending a given
1253
+ # part are 2.6 behavior (#835), so they ride in the
1254
+ # snapshot only, and the one v1 knob that reaches them is
1255
+ # deleting the title. Lexicon does not check this pair
1256
+ # (classify reads a member only where it is a title too),
1257
+ # so the intersection is kept for `_snapshot() ==
1258
+ # Lexicon.default()` legibility, as conjunctions_ambiguous'.
1259
+ after_given_titles=AFTER_GIVEN_TITLES & titles,
1260
+ # likewise no v1 manager: a suffix word that is also a
1261
+ # name is 2.7 behavior (#752), so it rides in the
1262
+ # snapshot only. Intersect with the word set, as the
1263
+ # tails are: Lexicon enforces the subset, and deleting
1264
+ # the suffix word is the one v1 knob, which turns the
1265
+ # reading off. agnomens is consulted only inside it, so
1266
+ # its intersection is kept for legibility alone.
1267
+ # A v1 caller who added such a word to suffix_acronyms
1268
+ # had it read as a suffix through the acronym branch,
1269
+ # so the word leaves the gate rather than raising at
1270
+ # Lexicon's word-gate check (folded, as the two
1271
+ # cross-set computations above are).
1272
+ suffix_words_ambiguous=frozenset(
1273
+ w for w in SUFFIX_WORDS_AMBIGUOUS & suffix_words
1274
+ if _normalize(w) not in {
1275
+ _normalize(a) for a in acronyms - ambiguous_acronyms}),
1276
+ agnomens=AGNOMENS & SUFFIX_WORDS_AMBIGUOUS & suffix_words,
1277
+ # likewise no v1 manager: the title phrase (rules/H9) is
1278
+ # 2.7 behavior (#752), so its sets ride in the snapshot
1279
+ # only. A title that may be a given name is not in
1280
+ # TITLES, so v1's `titles` holds one only where 2.6.0
1281
+ # listed it (`moved`, above, read by the phrase), and one
1282
+ # 2.6.0 never listed that a v1 caller ADDS to `titles` is
1283
+ # a title wherever it stands, as it was in 1.4
1284
+ # (`c.titles.add("dean")`, the documented recipe): the
1285
+ # phrase test reads a word of titles_ambiguous only where
1286
+ # it is not in titles. The
1287
+ # knob that reaches the other three is deleting a title,
1288
+ # which turns its modifier, style or phrase reading off
1289
+ # with it; Lexicon checks none of them, so the
1290
+ # intersections are kept for legibility, as
1291
+ # after_given_titles' is.
1292
+ # ...save where the caller emptied `titles`, v1's way to
1293
+ # turn title detection off (customize.rst). A caller who
1294
+ # REPLACES the list keeps the phrase reading of the words
1295
+ # 2.6.0 never listed: v1 has no knob for them, and what a
1296
+ # replaced list means for words it never held is not
1297
+ # something it says. A moved word reads by whether the
1298
+ # replaced list holds it, as any v1 title does
1299
+ titles_ambiguous=phrase_only,
1300
+ title_modifiers=TITLE_MODIFIERS & any_title,
1301
+ title_styles=TITLE_STYLES & any_title,
1302
+ title_phrases=frozenset(
1303
+ p for p in TITLE_PHRASES
1304
+ if all(w in any_title for w in p.split(" "))),
1210
1305
  # TupleManager is dict[str, object] (v1 parity: values were
1211
1306
  # never statically str-typed); every real entry is a str,
1212
1307
  # 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,10 @@ _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
+ "given_name_abbreviations",
29
+ "maiden_markers", "surnames", "honorific_tails", "after_given_titles",
30
+ "suffix_words_ambiguous", "agnomens", "titles_ambiguous",
31
+ "title_modifiers", "title_styles", "title_phrases",
29
32
  )
30
33
 
31
34
  #: (marker, base, why) triples. Each marker QUALIFIES how entries of
@@ -55,21 +58,27 @@ _VOCAB_FIELDS = (
55
58
  #: as a tail would peel a period-gated word off a real name), and
56
59
  #: nothing needs the acronym half: the shipped tails are CJK
57
60
  #: 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.
61
+ #: The shipped vocabulary holds the relation by construction: its
62
+ #: build puts a word in GLUED_HONORIFICS only through an `attached`
63
+ #: property on a word reading (tools/vocab/build.py). The check here
64
+ #: is the only thing that holds a caller's own.
65
65
  #:
66
- #: Two fields are deliberately NOT here and have no check of their own.
66
+ #: Three fields are deliberately NOT here and have no check of their own.
67
67
  #: given_name_titles -- see the note in __post_init__ for why every
68
68
  #: attempt at one rejected working configurations. conjunctions_ambiguous
69
69
  #: -- an orphan is never consulted because both the classify fork and
70
70
  #: its ambiguity emitter require the base entry as well, so an orphan
71
71
  #: decides nothing, and AGENTS.md's invariants rule guards harm, not
72
- #: no-ops.
72
+ #: no-ops. after_given_titles -- the same: classify tags a word from it
73
+ #: only where the word is in titles too, so an orphan is never read.
74
+ #: agnomens -- the same: classify reads it only for a word already in
75
+ #: suffix_words_ambiguous, so an orphan decides nothing.
76
+ #: title_modifiers, title_styles and title_phrases -- the same: the
77
+ #: title phrase test (rules/H9) reads them only for words that are
78
+ #: titles, so an orphan decides nothing. titles_ambiguous is no subset
79
+ #: at all: its words are titles only inside a phrase, so a caller lists
80
+ #: one there without listing it in titles, and one in both is a title
81
+ #: everywhere.
73
82
  _SUBSET_FIELDS = (
74
83
  ("particles_ambiguous", "particles",
75
84
  "an orphan emits a spurious particle-or-given ambiguity"),
@@ -77,6 +86,8 @@ _SUBSET_FIELDS = (
77
86
  "an orphan silently becomes a period-gated suffix"),
78
87
  ("honorific_tails", "suffix_words",
79
88
  "an orphan splits the name and leaves the tail inside it"),
89
+ ("suffix_words_ambiguous", "suffix_words",
90
+ "an orphan silently becomes a suffix gated by the count"),
80
91
  )
81
92
 
82
93
 
@@ -100,7 +111,7 @@ _SUBSET_FIELDS = (
100
111
  #: by a genuine multi-token lookahead instead --
101
112
  #: _pipeline._vocab.maiden_marker_run, longest first -- and only the
102
113
  #: STORAGE rule is shared with titles.
103
- _PHRASE_FIELDS = ("given_name_titles", "maiden_markers")
114
+ _PHRASE_FIELDS = ("given_name_titles", "maiden_markers", "title_phrases")
104
115
 
105
116
 
106
117
  #: Every character the parser reads as a full stop at a WORD'S EDGE
@@ -569,12 +580,14 @@ class Lexicon:
569
580
  Entries are normalized at construction -- lowercased, NFC-composed,
570
581
  edge full stops stripped -- so matching is case-insensitive.
571
582
  Vocabulary entries are single words -- a multi-word entry warns at
572
- construction and can never match. Two fields are exempt, and they
583
+ construction and can never match. Three fields are exempt, and they
573
584
  differ in HOW they match: ``given_name_titles`` is looked up
574
585
  against the run of words the parse has ALREADY read as titles --
575
- the whole run space-joined, or that run's last word -- while
586
+ the whole run space-joined, or that run's last word --
576
587
  ``maiden_markers`` is matched by lookahead, longest first, over
577
- words that need not be markers on their own (``"z domu"``).
588
+ words that need not be markers on their own (``"z domu"``), and
589
+ ``title_phrases`` holds two-word phrases, stored space-joined and
590
+ matched against two adjacent title words (``"chief justice"``).
578
591
  Field docs below show examples, not full
579
592
  contents; inspect any field's shipped vocabulary directly, e.g.
580
593
  ``Lexicon.default().conjunctions``."""
@@ -583,7 +596,7 @@ class Lexicon:
583
596
  #: list: :data:`~nameparser.config.titles.TITLES`.
584
597
  titles: frozenset[str] = frozenset()
585
598
  #: Titles whose single name word, before or after them, reads as a
586
- #: GIVEN name ("sheikh", "sister", ...) rather than a family name. Full
599
+ #: GIVEN name ("sir", "sister", ...) rather than a family name. Full
587
600
  #: default list: :data:`~nameparser.config.titles.GIVEN_NAME_TITLES`.
588
601
  given_name_titles: frozenset[str] = frozenset()
589
602
  #: Post-nominal acronym suffixes, matched with or without periods
@@ -658,6 +671,17 @@ class Lexicon:
658
671
  #: Full default list:
659
672
  #: :data:`~nameparser.config.bound_given_names.BOUND_GIVEN_NAMES`.
660
673
  bound_given_names: frozenset[str] = frozenset()
674
+ #: Given names written short ("chas" for Charles, "wm" for William,
675
+ #: Filipino "ma" for María). Period-marked and opening the part of
676
+ #: the name that carries the given name, an entry reads as that
677
+ #: given name, where an unlisted abbreviation there reads as a
678
+ #: title by its shape (rules/H2): "Chas. Smith" reads given
679
+ #: "Chas.", "Xyz. Smith" title "Xyz.". An entry also in ``titles``
680
+ #: is a title there, which is what listing it as one asks; anywhere
681
+ #: else an entry is read as it would be unlisted. Full default
682
+ #: list:
683
+ #: :data:`~nameparser.config.given_names.GIVEN_NAME_ABBREVIATIONS`.
684
+ given_name_abbreviations: frozenset[str] = frozenset()
661
685
  #: Marker words introducing a birth surname, routed to the maiden
662
686
  #: field ("née", "geb.", "rozená", ...). An entry may be a PHRASE
663
687
  #: ("z domu"): entries are matched by lookahead, longest first, so
@@ -692,6 +716,73 @@ class Lexicon:
692
716
  #: vocabulary is. Full default list:
693
717
  #: :data:`~nameparser.config.suffixes.GLUED_HONORIFICS`.
694
718
  honorific_tails: frozenset[str] = frozenset()
719
+ #: Salutations that read as a title where one ENDS the given part
720
+ #: after a family comma, written bare: "Doe, Jane Mrs" reads title
721
+ #: "Mrs" where an office or rank word there stays a middle name
722
+ #: ("Doe, John Pope"), rules/H8. A period-marked title word reads as
723
+ #: a title there whether listed here or not (rules/H5). Entries need
724
+ #: not also be in ``titles``; one that is not is never consulted.
725
+ #: Full default list:
726
+ #: :data:`~nameparser.config.titles.AFTER_GIVEN_TITLES`.
727
+ after_given_titles: frozenset[str] = frozenset()
728
+ #: Subset of suffix_words that are also borne as names ("fils",
729
+ #: "neto"). Read as a suffix only where the name has words to
730
+ #: spare, as an ambiguous acronym is: "Pedro Alves Neto" ends in a
731
+ #: suffix, "Ana Neto" keeps her family name, and either reading is
732
+ #: reported (rules/S11). Written in Title case in a name of mixed
733
+ #: case it leans to the name even with words to spare, and in lower
734
+ #: case it is the suffix after a comma that names the family,
735
+ #: unless it is also in ``agnomens``. Full default list:
736
+ #: :data:`~nameparser.config.suffixes.SUFFIX_WORDS_AMBIGUOUS`.
737
+ suffix_words_ambiguous: frozenset[str] = frozenset()
738
+ #: Subset of suffix_words_ambiguous whose writing says nothing:
739
+ #: an agnomen ("filho", "neto") is written in the case of the name
740
+ #: around it, so only the words to spare decide (rules/S11). An
741
+ #: entry outside suffix_words_ambiguous is never consulted. Full
742
+ #: default list: :data:`~nameparser.config.suffixes.AGNOMENS`.
743
+ agnomens: frozenset[str] = frozenset()
744
+ #: Titles that may also be a given name ("dean", "justice",
745
+ #: "major"), each read as a title only inside a title phrase: after a
746
+ #: modifier, a style, or a title it stands in ``title_phrases``
747
+ #: behind; or before "of" or a title it stands in ``title_phrases``
748
+ #: in front of. Elsewhere it is a name word, and where it opens the
749
+ #: name the parse reports ``title-or-name`` if a phrase of
750
+ #: ``title_phrases`` the parse can read holds it (rules/H9), so "Grace Smith", which no
751
+ #: phrase holds, reports nothing, and emptying ``title_phrases``
752
+ #: silences the report: "Dean Smith"
753
+ #: and "Mr Major Smith" read given "Dean" and "Major", "Dean of
754
+ #: Students Smith" and "Major General Smith" a title. An entry also
755
+ #: in ``titles`` is a title wherever it stands, which is what listing
756
+ #: it there asks, so the default lists these words here alone. Full
757
+ #: default list:
758
+ #: :data:`~nameparser.config.titles.TITLES_AMBIGUOUS`.
759
+ titles_ambiguous: frozenset[str] = frozenset()
760
+ #: Titles that only qualify a head title ("vice", "wing", "grand").
761
+ #: At the head of the name one is a title only with a head title
762
+ #: behind it in its run of titles, so "Wing Commander Lau" keeps the
763
+ #: title where "Wing Lau" reads "Wing" as the given name; after a
764
+ #: comma it is read as any title is (rules/H9), save one also in
765
+ #: ``titles_ambiguous``, which is read everywhere as at the head. One
766
+ #: standing before a word of ``titles_ambiguous`` puts that word in
767
+ #: a title phrase. Entries in neither ``titles`` nor
768
+ #: ``titles_ambiguous`` are never consulted. Full default
769
+ #: list: :data:`~nameparser.config.titles.TITLE_MODIFIERS`.
770
+ title_modifiers: frozenset[str] = frozenset()
771
+ #: Honorific styles ("hon", "excellency", "highness"): one standing
772
+ #: before a word of ``titles_ambiguous`` puts that word in a title
773
+ #: phrase, as in "The Hon. Justice Smith" (rules/H9). Entries in
774
+ #: neither ``titles`` nor ``titles_ambiguous`` are never consulted.
775
+ #: Full default list:
776
+ #: :data:`~nameparser.config.titles.TITLE_STYLES`.
777
+ title_styles: frozenset[str] = frozenset()
778
+ #: Two-word title phrases, a title and the title it qualifies
779
+ #: ("chief justice", "major general"), stored space-joined: a word
780
+ #: of ``titles_ambiguous`` on either side of one is in a title
781
+ #: phrase (rules/H9). A phrase is consulted only if it is two
782
+ #: words, each in ``titles`` or ``titles_ambiguous``. Full default
783
+ #: list:
784
+ #: :data:`~nameparser.config.titles.TITLE_PHRASES`.
785
+ title_phrases: frozenset[str] = frozenset()
695
786
  #: Lowercase word -> case mask used by capitalized(): the word's
696
787
  #: own letters and digits, each in the case it takes ("phd" ->
697
788
  #: "PhD"), laid over the word as written, so the one entry
@@ -739,9 +830,10 @@ class Lexicon:
739
830
  # the expensive one -- three working configurations broken
740
831
  # across two attempts. Do not add a third.
741
832
  #
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.
833
+ # The v2 form of the vocabulary build's check that a bound given
834
+ # name which is also a particle may be given (tools/vocab/build.py,
835
+ # check 9). The build guards the shipped data; this guards
836
+ # vocabulary a caller supplies.
745
837
  contradictory = (
746
838
  self.bound_given_names & self.particles) - self.particles_ambiguous
747
839
  if contradictory:
@@ -765,6 +857,22 @@ class Lexicon:
765
857
  f"period gate suffix_acronyms_ambiguous exists to impose"
766
858
  )
767
859
 
860
+ # the mirror of the check above: suffix_as_written ORs the
861
+ # unambiguous acronym branch too, so a gated suffix word listed
862
+ # there is claimed as a suffix with no count ('Ana Neto' would
863
+ # lose her family name, rules/S11)
864
+ word_gate_bypassed = self.suffix_words_ambiguous & (
865
+ self.suffix_acronyms - self.suffix_acronyms_ambiguous)
866
+ if word_gate_bypassed:
867
+ raise ValueError(
868
+ f"an ambiguous suffix word must not also be an unambiguous "
869
+ f"suffix acronym; in both: "
870
+ f"{', '.join(sorted(word_gate_bypassed))}. The acronym "
871
+ f"branch matches it outright, which bypasses the count "
872
+ f"suffix_words_ambiguous exists to impose; drop it from "
873
+ f"suffix_acronyms or from suffix_words_ambiguous"
874
+ )
875
+
768
876
  # -- constructors ----------------------------------------------------
769
877
 
770
878
  @classmethod
@@ -820,7 +928,10 @@ class Lexicon:
820
928
  # Lexicon field layout (version skew) -- silently loading it
821
929
  # would defer the failure to some distant attribute read.
822
930
  # Message kept in sync with _types._guarded_setstate by design
823
- # (layering keeps this module import-free of _types).
931
+ # (layering keeps this module import-free of _types). Unlike
932
+ # that guard it admits no field added later (`ADDED_IN`, 2.7):
933
+ # every Lexicon field is still required, so a release adding one
934
+ # breaks older Lexicon pickles until this learns the marker.
824
935
  expected = {f.name for f in dataclasses.fields(Lexicon)} - {"_cap_map"}
825
936
  if set(state) != expected:
826
937
  missing = ", ".join(sorted(expected - set(state))) or "none"
@@ -937,17 +1048,20 @@ class Lexicon:
937
1048
  def _default_lexicon() -> Lexicon:
938
1049
  # v1 data modules are the single source of vocabulary through 2.x.
939
1050
  from nameparser.config.bound_given_names import BOUND_GIVEN_NAMES
1051
+ from nameparser.config.given_names import GIVEN_NAME_ABBREVIATIONS
940
1052
  from nameparser.config.capitalization import CAPITALIZATION_EXCEPTIONS
941
1053
  from nameparser.config.conjunctions import (
942
1054
  CONJUNCTIONS, CONJUNCTIONS_AMBIGUOUS)
943
1055
  from nameparser.config.maiden_markers import MAIDEN_MARKERS
944
1056
  from nameparser.config.particles import NON_GIVEN_NAME_PARTICLES, PARTICLES
945
1057
  from nameparser.config.suffixes import (
946
- GLUED_HONORIFICS, SUFFIX_ACRONYMS, SUFFIX_ACRONYMS_AMBIGUOUS,
947
- SUFFIX_WORDS,
1058
+ AGNOMENS, GLUED_HONORIFICS, SUFFIX_ACRONYMS, SUFFIX_ACRONYMS_AMBIGUOUS,
1059
+ SUFFIX_WORDS, SUFFIX_WORDS_AMBIGUOUS,
948
1060
  )
949
1061
  from nameparser.config.surnames import KOREAN_SURNAMES
950
- from nameparser.config.titles import GIVEN_NAME_TITLES, TITLES
1062
+ from nameparser.config.titles import (
1063
+ AFTER_GIVEN_TITLES, GIVEN_NAME_TITLES, TITLE_MODIFIERS, TITLE_PHRASES,
1064
+ TITLE_STYLES, TITLES, TITLES_AMBIGUOUS)
951
1065
 
952
1066
  # every vocabulary constant is a frozenset since #293, so each one
953
1067
  # feeds its strictly-typed frozenset[str] field as it stands -- and
@@ -972,9 +1086,17 @@ def _default_lexicon() -> Lexicon:
972
1086
  conjunctions=CONJUNCTIONS,
973
1087
  conjunctions_ambiguous=CONJUNCTIONS_AMBIGUOUS,
974
1088
  bound_given_names=BOUND_GIVEN_NAMES,
1089
+ given_name_abbreviations=GIVEN_NAME_ABBREVIATIONS,
975
1090
  maiden_markers=MAIDEN_MARKERS,
976
1091
  surnames=KOREAN_SURNAMES,
977
1092
  honorific_tails=GLUED_HONORIFICS,
1093
+ after_given_titles=AFTER_GIVEN_TITLES,
1094
+ suffix_words_ambiguous=SUFFIX_WORDS_AMBIGUOUS,
1095
+ agnomens=AGNOMENS,
1096
+ titles_ambiguous=TITLES_AMBIGUOUS,
1097
+ title_modifiers=TITLE_MODIFIERS,
1098
+ title_styles=TITLE_STYLES,
1099
+ title_phrases=TITLE_PHRASES,
978
1100
  # pass canonical pair-tuples so this strictly-typed call site never
979
1101
  # feeds a Mapping to the tuple-annotated field; __post_init__
980
1102
  # 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
@@ -21,12 +21,42 @@ tests/v2/pipeline/test_assemble.py.
21
21
  """
22
22
  from __future__ import annotations
23
23
 
24
- from nameparser._pipeline._state import FIELD_READING, NAME_ROLES, ParseState
24
+ from nameparser._pipeline._state import (
25
+ FIELD_READING, NAME_ROLES, Beside, Deferred, ParseState,
26
+ PendingAmbiguity,
27
+ )
25
28
  from nameparser._types import (
26
- Ambiguity, AmbiguityKind, ParsedName, Role, Token,
29
+ FOLDED_TAG, Ambiguity, AmbiguityKind, ParsedName, Reading, Role, Token,
27
30
  )
28
31
 
29
32
 
33
+ def _alternative(pending: PendingAmbiguity, materialized: tuple[Token, ...],
34
+ final: dict[int, Token],
35
+ middle_as_family: bool) -> Reading | None:
36
+ """The public Reading of a report's PendingReading, its field named
37
+ from the final roles where the emitter deferred it (O5's other
38
+ primary field) and folded as `middle_as_family` folds every middle
39
+ (rules/O6's neighbour, the policy's own fold)."""
40
+ alt = pending.alternative
41
+ if alt is None or not materialized:
42
+ return None
43
+ role = alt.role
44
+ if role is Deferred.OTHER_PRIMARY:
45
+ role = (Role.FAMILY if materialized[0].role is Role.GIVEN
46
+ else Role.GIVEN)
47
+ elif isinstance(role, Beside) and role.index in final:
48
+ front = final[role.index]
49
+ role = (role.unfolded
50
+ if FOLDED_TAG in front.tags and role.unfolded is not None
51
+ else front.role)
52
+ elif isinstance(role, (Deferred, Beside)):
53
+ # assign found no name position to name it from
54
+ return None
55
+ if middle_as_family and role is Role.MIDDLE:
56
+ role = Role.FAMILY
57
+ return Reading(alt.kind, role, alt.source)
58
+
59
+
30
60
  # rules/A2: "a name with no name content parses to the empty
31
61
  # name — every field empty, false as a boolean — while ambiguities
32
62
  # born from its punctuation survive on the empty result"
@@ -116,7 +146,7 @@ def assemble(state: ParseState) -> ParsedName:
116
146
  and any(t.role in (Role.SUFFIX, Role.TITLE)
117
147
  for t in materialized)):
118
148
  continue
119
- # rules/P3: "unless no name word of its part stands to its
149
+ # rules/P8: "unless no name word of its part stands to its
120
150
  # right" -- nothing after the letter in its segment, or only
121
151
  # suffixes, titles, a maiden clause or a nickname: it connects
122
152
  # nothing, so the report goes (#721). Asked here for the reason
@@ -143,7 +173,10 @@ def assemble(state: ParseState) -> ParsedName:
143
173
  continue
144
174
  detail = (f"{detail}{FIELD_READING[materialized[0].role]}"
145
175
  f"{pending.field_tail}")
146
- ambiguities.append(Ambiguity(pending.kind, detail, materialized))
176
+ ambiguities.append(Ambiguity(
177
+ pending.kind, detail, materialized,
178
+ _alternative(pending, materialized, final,
179
+ state.policy.middle_as_family)))
147
180
  return ParsedName(original=state.original,
148
181
  tokens=tuple(final.values()),
149
182
  ambiguities=tuple(ambiguities))