nameparser 2.4.2__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 (243) hide show
  1. {nameparser-2.4.2/nameparser.egg-info → nameparser-2.6.0}/PKG-INFO +9 -11
  2. {nameparser-2.4.2 → nameparser-2.6.0}/README.rst +7 -8
  3. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_config_shim.py +12 -3
  4. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_facade.py +9 -8
  5. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_lexicon.py +40 -28
  6. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_parser.py +8 -8
  7. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_pipeline/_assemble.py +24 -5
  8. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_pipeline/_assign.py +164 -76
  9. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_pipeline/_classify.py +137 -34
  10. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_pipeline/_comma.py +277 -76
  11. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_pipeline/_extract.py +18 -15
  12. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_pipeline/_group.py +141 -255
  13. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_pipeline/_pieces.py +171 -105
  14. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_pipeline/_post_rules.py +195 -134
  15. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_pipeline/_script_segment.py +49 -105
  16. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_pipeline/_segment.py +31 -14
  17. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_pipeline/_state.py +11 -10
  18. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_pipeline/_tokenize.py +32 -20
  19. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_pipeline/_vocab.py +208 -67
  20. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_policy.py +57 -20
  21. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_render.py +142 -59
  22. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_types.py +26 -15
  23. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_version.py +4 -4
  24. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/config/__init__.py +3 -1
  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.4.2 → nameparser-2.6.0}/nameparser/config/maiden_markers.py +10 -27
  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.4.2 → nameparser-2.6.0}/nameparser/locales/ja.py +2 -2
  35. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/locales/zh.py +70 -6
  36. nameparser-2.6.0/nameparser/vocab/ak.toml +5 -0
  37. nameparser-2.6.0/nameparser/vocab/am.toml +9 -0
  38. nameparser-2.6.0/nameparser/vocab/ar.toml +526 -0
  39. nameparser-2.6.0/nameparser/vocab/as.toml +19 -0
  40. nameparser-2.6.0/nameparser/vocab/be.toml +23 -0
  41. nameparser-2.6.0/nameparser/vocab/bg.toml +61 -0
  42. nameparser-2.6.0/nameparser/vocab/bin.toml +9 -0
  43. nameparser-2.6.0/nameparser/vocab/bn.toml +182 -0
  44. nameparser-2.6.0/nameparser/vocab/bo.toml +13 -0
  45. nameparser-2.6.0/nameparser/vocab/bs.toml +29 -0
  46. nameparser-2.6.0/nameparser/vocab/ca.toml +8 -0
  47. nameparser-2.6.0/nameparser/vocab/cs.toml +51 -0
  48. nameparser-2.6.0/nameparser/vocab/da.toml +41 -0
  49. nameparser-2.6.0/nameparser/vocab/de.toml +207 -0
  50. nameparser-2.6.0/nameparser/vocab/el.toml +33 -0
  51. nameparser-2.6.0/nameparser/vocab/en.toml +4955 -0
  52. nameparser-2.6.0/nameparser/vocab/es.toml +101 -0
  53. nameparser-2.6.0/nameparser/vocab/et.toml +18 -0
  54. nameparser-2.6.0/nameparser/vocab/fa.toml +37 -0
  55. nameparser-2.6.0/nameparser/vocab/ff.toml +5 -0
  56. nameparser-2.6.0/nameparser/vocab/fi.toml +26 -0
  57. nameparser-2.6.0/nameparser/vocab/fil.toml +20 -0
  58. nameparser-2.6.0/nameparser/vocab/fr.toml +183 -0
  59. nameparser-2.6.0/nameparser/vocab/ga.toml +63 -0
  60. nameparser-2.6.0/nameparser/vocab/gd.toml +9 -0
  61. nameparser-2.6.0/nameparser/vocab/gu.toml +13 -0
  62. nameparser-2.6.0/nameparser/vocab/ha.toml +17 -0
  63. nameparser-2.6.0/nameparser/vocab/he.toml +133 -0
  64. nameparser-2.6.0/nameparser/vocab/hi.toml +211 -0
  65. nameparser-2.6.0/nameparser/vocab/hr.toml +41 -0
  66. nameparser-2.6.0/nameparser/vocab/hu.toml +17 -0
  67. nameparser-2.6.0/nameparser/vocab/hy.toml +23 -0
  68. nameparser-2.6.0/nameparser/vocab/id.toml +5 -0
  69. nameparser-2.6.0/nameparser/vocab/is.toml +25 -0
  70. nameparser-2.6.0/nameparser/vocab/it.toml +194 -0
  71. nameparser-2.6.0/nameparser/vocab/ja.toml +144 -0
  72. nameparser-2.6.0/nameparser/vocab/ka.toml +21 -0
  73. nameparser-2.6.0/nameparser/vocab/kk.toml +7 -0
  74. nameparser-2.6.0/nameparser/vocab/km.toml +11 -0
  75. nameparser-2.6.0/nameparser/vocab/kn.toml +5 -0
  76. nameparser-2.6.0/nameparser/vocab/ko.toml +88 -0
  77. nameparser-2.6.0/nameparser/vocab/kok.toml +7 -0
  78. nameparser-2.6.0/nameparser/vocab/lo.toml +5 -0
  79. nameparser-2.6.0/nameparser/vocab/lt.toml +35 -0
  80. nameparser-2.6.0/nameparser/vocab/lv.toml +17 -0
  81. nameparser-2.6.0/nameparser/vocab/mi.toml +5 -0
  82. nameparser-2.6.0/nameparser/vocab/mk.toml +15 -0
  83. nameparser-2.6.0/nameparser/vocab/ml.toml +9 -0
  84. nameparser-2.6.0/nameparser/vocab/mn.toml +11 -0
  85. nameparser-2.6.0/nameparser/vocab/mr.toml +91 -0
  86. nameparser-2.6.0/nameparser/vocab/ms.toml +52 -0
  87. nameparser-2.6.0/nameparser/vocab/mul.toml +58 -0
  88. nameparser-2.6.0/nameparser/vocab/my.toml +5 -0
  89. nameparser-2.6.0/nameparser/vocab/nb.toml +45 -0
  90. nameparser-2.6.0/nameparser/vocab/ne.toml +11 -0
  91. nameparser-2.6.0/nameparser/vocab/nl.toml +147 -0
  92. nameparser-2.6.0/nameparser/vocab/nn.toml +5 -0
  93. nameparser-2.6.0/nameparser/vocab/or.toml +9 -0
  94. nameparser-2.6.0/nameparser/vocab/pa.toml +29 -0
  95. nameparser-2.6.0/nameparser/vocab/pl.toml +24 -0
  96. nameparser-2.6.0/nameparser/vocab/pt.toml +70 -0
  97. nameparser-2.6.0/nameparser/vocab/ro.toml +17 -0
  98. nameparser-2.6.0/nameparser/vocab/ru.toml +88 -0
  99. nameparser-2.6.0/nameparser/vocab/sd.toml +21 -0
  100. nameparser-2.6.0/nameparser/vocab/shn.toml +5 -0
  101. nameparser-2.6.0/nameparser/vocab/si.toml +5 -0
  102. nameparser-2.6.0/nameparser/vocab/sk.toml +29 -0
  103. nameparser-2.6.0/nameparser/vocab/sl.toml +29 -0
  104. nameparser-2.6.0/nameparser/vocab/sr.toml +52 -0
  105. nameparser-2.6.0/nameparser/vocab/sv.toml +38 -0
  106. nameparser-2.6.0/nameparser/vocab/sw.toml +7 -0
  107. nameparser-2.6.0/nameparser/vocab/ta.toml +17 -0
  108. nameparser-2.6.0/nameparser/vocab/te.toml +9 -0
  109. nameparser-2.6.0/nameparser/vocab/th.toml +9 -0
  110. nameparser-2.6.0/nameparser/vocab/ti.toml +9 -0
  111. nameparser-2.6.0/nameparser/vocab/tr.toml +26 -0
  112. nameparser-2.6.0/nameparser/vocab/uk.toml +54 -0
  113. nameparser-2.6.0/nameparser/vocab/und.toml +21 -0
  114. nameparser-2.6.0/nameparser/vocab/ur.toml +33 -0
  115. nameparser-2.6.0/nameparser/vocab/vi.toml +9 -0
  116. nameparser-2.6.0/nameparser/vocab/yi.toml +5 -0
  117. nameparser-2.6.0/nameparser/vocab/yo.toml +15 -0
  118. nameparser-2.6.0/nameparser/vocab/zh.toml +153 -0
  119. nameparser-2.6.0/nameparser/vocab/zu.toml +6 -0
  120. {nameparser-2.4.2 → nameparser-2.6.0/nameparser.egg-info}/PKG-INFO +9 -11
  121. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser.egg-info/SOURCES.txt +100 -0
  122. {nameparser-2.4.2 → nameparser-2.6.0}/pyproject.toml +16 -4
  123. {nameparser-2.4.2 → nameparser-2.6.0}/tests/conftest.py +28 -0
  124. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_capitalization.py +16 -16
  125. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_initials.py +4 -4
  126. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_particles.py +3 -2
  127. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_suffixes.py +11 -11
  128. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_titles.py +3 -3
  129. nameparser-2.6.0/tests/v2/_differential_fixtures.py +127 -0
  130. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/cases.py +2171 -425
  131. nameparser-2.6.0/tests/v2/design_docs.py +498 -0
  132. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/pipeline/test_assemble.py +5 -2
  133. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/pipeline/test_assign.py +2 -2
  134. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/pipeline/test_classify.py +55 -21
  135. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/pipeline/test_comma.py +10 -10
  136. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/pipeline/test_group.py +36 -30
  137. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/pipeline/test_pieces.py +32 -25
  138. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/pipeline/test_post_rules.py +65 -20
  139. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/pipeline/test_script_segment.py +4 -2
  140. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/pipeline/test_segment.py +2 -2
  141. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/pipeline/test_tokenize.py +43 -0
  142. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/pipeline/test_vocab.py +42 -8
  143. nameparser-2.6.0/tests/v2/rules_budget.py +154 -0
  144. nameparser-2.6.0/tests/v2/rules_doc.py +365 -0
  145. nameparser-2.6.0/tests/v2/rules_skeleton.py +482 -0
  146. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_benchmark.py +28 -19
  147. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_cases.py +77 -7
  148. nameparser-2.6.0/tests/v2/test_cldr_agreement.py +573 -0
  149. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_config_shim.py +19 -1
  150. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_contracts.py +15 -11
  151. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_corpora.py +23 -13
  152. nameparser-2.6.0/tests/v2/test_corpus_build.py +182 -0
  153. nameparser-2.6.0/tests/v2/test_design_docs.py +556 -0
  154. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_differential.py +20 -6
  155. nameparser-2.6.0/tests/v2/test_doc_citations.py +659 -0
  156. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_doc_spellings.py +1 -1
  157. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_facade.py +31 -43
  158. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_facade_cases.py +55 -1
  159. nameparser-2.6.0/tests/v2/test_gnd_corpus.py +301 -0
  160. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_lexicon.py +70 -7
  161. nameparser-2.6.0/tests/v2/test_local_agreement.py +300 -0
  162. nameparser-2.6.0/tests/v2/test_local_corpora.py +4106 -0
  163. nameparser-2.6.0/tests/v2/test_local_jrc.py +548 -0
  164. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_locales.py +17 -14
  165. nameparser-2.6.0/tests/v2/test_name_usage_tool.py +2295 -0
  166. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_order_correspondence.py +19 -19
  167. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_parser.py +91 -42
  168. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_policy.py +8 -4
  169. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_properties.py +295 -58
  170. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_regex_sync.py +9 -2
  171. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_render.py +222 -49
  172. nameparser-2.6.0/tests/v2/test_rules_budget.py +182 -0
  173. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_rules_doc.py +117 -8
  174. nameparser-2.6.0/tests/v2/test_rules_doc_grammar.py +236 -0
  175. nameparser-2.6.0/tests/v2/test_rules_skeleton.py +489 -0
  176. nameparser-2.6.0/tests/v2/test_snapshot.py +150 -0
  177. nameparser-2.6.0/tests/v2/test_vocab.py +885 -0
  178. nameparser-2.6.0/tests/v2/test_wikidata_corpus.py +429 -0
  179. nameparser-2.6.0/tests/v2/test_wikidata_native_corpus.py +480 -0
  180. nameparser-2.6.0/tests/v2/test_zh_surnames_tool.py +145 -0
  181. nameparser-2.4.2/nameparser/config/_invariants.py +0 -53
  182. nameparser-2.4.2/nameparser/config/bound_given_names.py +0 -60
  183. nameparser-2.4.2/nameparser/config/capitalization.py +0 -76
  184. nameparser-2.4.2/nameparser/config/conjunctions.py +0 -92
  185. nameparser-2.4.2/nameparser/config/particles.py +0 -303
  186. nameparser-2.4.2/nameparser/config/suffixes.py +0 -1023
  187. nameparser-2.4.2/nameparser/config/titles.py +0 -1026
  188. nameparser-2.4.2/tests/v2/_differential_fixtures.py +0 -55
  189. nameparser-2.4.2/tests/v2/rules_doc.py +0 -269
  190. nameparser-2.4.2/tests/v2/test_cldr_agreement.py +0 -277
  191. nameparser-2.4.2/tests/v2/test_doc_citations.py +0 -393
  192. nameparser-2.4.2/tests/v2/test_name_usage_tool.py +0 -303
  193. nameparser-2.4.2/tests/v2/test_rules_doc_grammar.py +0 -168
  194. nameparser-2.4.2/tests/v2/test_snapshot.py +0 -48
  195. {nameparser-2.4.2 → nameparser-2.6.0}/AUTHORS +0 -0
  196. {nameparser-2.4.2 → nameparser-2.6.0}/LICENSE +0 -0
  197. {nameparser-2.4.2 → nameparser-2.6.0}/MANIFEST.in +0 -0
  198. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/__init__.py +0 -0
  199. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/__main__.py +0 -0
  200. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_locale.py +0 -0
  201. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/_pipeline/__init__.py +0 -0
  202. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/config/_deprecated.py +0 -0
  203. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/config/bound_first_names.py +0 -0
  204. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/config/prefixes.py +0 -0
  205. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/config/regexes.py +0 -0
  206. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/config/surnames.py +0 -0
  207. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/locales/__init__.py +0 -0
  208. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/locales/ru.py +0 -0
  209. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/locales/tr_az.py +0 -0
  210. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/parser.py +0 -0
  211. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/py.typed +0 -0
  212. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser/util.py +0 -0
  213. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser.egg-info/dependency_links.txt +0 -0
  214. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser.egg-info/requires.txt +0 -0
  215. {nameparser-2.4.2 → nameparser-2.6.0}/nameparser.egg-info/top_level.txt +0 -0
  216. {nameparser-2.4.2 → nameparser-2.6.0}/setup.cfg +0 -0
  217. {nameparser-2.4.2 → nameparser-2.6.0}/tests/__init__.py +0 -0
  218. {nameparser-2.4.2 → nameparser-2.6.0}/tests/base.py +0 -0
  219. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_bound_given_names.py +0 -0
  220. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_brute_force.py +0 -0
  221. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_comma_variants.py +0 -0
  222. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_conjunctions.py +0 -0
  223. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_constants.py +0 -0
  224. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_east_slavic_patronymic_order.py +0 -0
  225. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_first_name.py +0 -0
  226. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_middle_name_as_last.py +0 -0
  227. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_nicknames.py +0 -0
  228. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_output_format.py +0 -0
  229. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_python_api.py +0 -0
  230. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_turkic_patronymic_order.py +0 -0
  231. {nameparser-2.4.2 → nameparser-2.6.0}/tests/test_variations.py +0 -0
  232. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/__init__.py +0 -0
  233. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/conftest.py +0 -0
  234. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/pipeline/__init__.py +0 -0
  235. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/pipeline/test_extract.py +0 -0
  236. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/pipeline/test_state.py +0 -0
  237. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_cldr_corpus.py +0 -0
  238. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_cli.py +0 -0
  239. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_config_aliases.py +0 -0
  240. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_layering.py +0 -0
  241. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_locale.py +0 -0
  242. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_reprs.py +0 -0
  243. {nameparser-2.4.2 → nameparser-2.6.0}/tests/v2/test_types.py +0 -0
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nameparser
3
- Version: 2.4.2
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,14 +35,13 @@ 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.4 is out.** It uses letter case to read post-nominal
40
- credentials much more reliably — ``Jack MA`` is Jack with an MA and
41
- ``Jack Ma`` is the surname Ma, and ``Jane Smith, PhD MA`` keeps both
42
- credentials together — recognizes more naming conventions (Catalan,
43
- Irish, Finnish, Malay and others), and writes credentials the usual way
44
- when it repairs case. The `release log
45
- <https://nameparser.readthedocs.io/en/latest/release_log.html>`__ has the
46
- details.
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
44
+ the details.
47
45
 
48
46
  Chinese, Japanese and Korean names written in their own scripts are read
49
47
  family-first, unspaced Korean names are split against the census surname
@@ -7,14 +7,13 @@ 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.4 is out.** It uses letter case to read post-nominal
11
- credentials much more reliably — ``Jack MA`` is Jack with an MA and
12
- ``Jack Ma`` is the surname Ma, and ``Jane Smith, PhD MA`` keeps both
13
- credentials together — recognizes more naming conventions (Catalan,
14
- Irish, Finnish, Malay and others), and writes credentials the usual way
15
- when it repairs case. The `release log
16
- <https://nameparser.readthedocs.io/en/latest/release_log.html>`__ has the
17
- details.
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
+ the details.
18
17
 
19
18
  Chinese, Japanese and Korean names written in their own scripts are read
20
19
  family-first, unspaced Korean names are split against the census surname
@@ -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
@@ -1109,7 +1110,7 @@ class Constants:
1109
1110
  # tests/v2/test_config_shim.py::test_snapshot_field_translation)
1110
1111
  # A capitalization_exceptions value that does not spell its key
1111
1112
  # is the one DECIDED exception to this method's never-raise
1112
- # rule (#459, decisions.md#R4 and #3-0-reevaluations).
1113
+ # rule (#459, decisions/R4 and #3-0-reevaluations).
1113
1114
  # _normpairs raises _MaskValueError for exactly that shape, and
1114
1115
  # the except clause re-spells its offered fix for a v1 caller.
1115
1116
  # Caught by TYPE, so every other Lexicon error passes through
@@ -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.
@@ -271,7 +271,7 @@ class HumanName:
271
271
  """Re-capitalize the current parse against the bound lexicon.
272
272
  force=None reads the bound Constants' render default
273
273
  (force_mixed_case_capitalization); the core's capitalized()
274
- implements the one-case gate (rules.md#R5, suffixes left out
274
+ implements the one-case gate (rules/R5, suffixes left out
275
275
  since #492) -- not
276
276
  re-implemented here."""
277
277
  self._resolve()
@@ -511,7 +511,7 @@ class HumanName:
511
511
  # answers -- the same fallback _render._cap_word takes for the
512
512
  # same tokens and through the same helper, which is why the
513
513
  # helper is imported rather than the predicate rewritten
514
- # (decisions.md#R4 for why one question and not two: whether a
514
+ # (decisions/R4 for why one question and not two: whether a
515
515
  # word is a connective is a fact the word can answer alone,
516
516
  # whether a particle is acting as one is a fact about the part).
517
517
  #
@@ -533,7 +533,7 @@ class HumanName:
533
533
  and UNJOINED_CONJUNCTION_TAG not in tok.tags)
534
534
 
535
535
  def _split_last(self) -> tuple[list[str], list[str]]:
536
- # rules.md#R2: "a name part whose every word is particle
536
+ # rules/R2: "a name part whose every word is particle
537
537
  # vocabulary is a part where none of them is doing a
538
538
  # particle's work" -- the all-particle guard
539
539
  # below is this rule, and predates its statement: v1 assumed a
@@ -617,7 +617,7 @@ class HumanName:
617
617
  #
618
618
  # Particles are NOT decided per token: _is_particle stays a
619
619
  # live vocabulary lookup, as _render._cap_word keeps it --
620
- # rules.md#R4 draws this boundary per question, not per field.
620
+ # rules/R4 draws this boundary per question, not per field.
621
621
  self._resolve()
622
622
  if tokens is None:
623
623
  # STRING PATH. split() rather than split(" ") because
@@ -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.md#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"),
@@ -129,7 +127,7 @@ _PHRASE_FIELDS = ("given_name_titles", "maiden_markers")
129
127
  #: it is roman five -- suffix -- where 'V.' stays a middle
130
128
  #: initial through that same carve-out; 'Jr。' simply reaches its
131
129
  #: entry, and 'Dr。' its title.
132
- #: decisions.md#cjk-full-stops carries the measurements.
130
+ #: decisions/cjk-full-stops carries the measurements.
133
131
  FULL_STOPS = "..。。"
134
132
 
135
133
 
@@ -152,7 +150,7 @@ def _normalize(word: str) -> str:
152
150
 
153
151
  NFC, since #322, and only for a non-ASCII word (ASCII is already
154
152
  NFC, and this runs per token on the parse path --
155
- decisions.md#parse-cost). NFD hangul decomposes to jamo and missed
153
+ decisions/parse-cost). NFD hangul decomposes to jamo and missed
156
154
  every vocabulary entry; composing at lookup AND at storage keeps
157
155
  the two symmetric, and no shipped entry changes under it (measured
158
156
  2026-09-09: fold every string member of every Lexicon.default()
@@ -175,7 +173,7 @@ def _normalize(word: str) -> str:
175
173
  word = stripped
176
174
 
177
175
 
178
- # rules.md#P7: "A one-letter word written with its period is an
176
+ # rules/P7: "A one-letter word written with its period is an
179
177
  # initial and not a particle"
180
178
  def _spells_an_initial(n: str, text: str) -> bool:
181
179
  """Whether `text` is ONE letter written with its period ('Ó.',
@@ -195,7 +193,7 @@ def _spells_an_initial(n: str, text: str) -> bool:
195
193
  -- its script table is _policy's, which this module may not import
196
194
  -- so a caller's one-character particle in such a script, written
197
195
  with a period, is vetoed where is_initial would call it no initial
198
- (decisions.md#P7). No shipped particle is one character of those
196
+ (decisions/P7). No shipped particle is one character of those
199
197
  scripts."""
200
198
  return len(n) == 1 and text[-1] == "."
201
199
 
@@ -224,7 +222,7 @@ def _fold_words(words: Iterable[str]) -> list[str]:
224
222
 
225
223
  map/filter rather than a comprehension: both are C calls where a
226
224
  comprehension is a Python frame on 3.11, and this runs on the parse
227
- path for every name carrying a title run (decisions.md#parse-cost).
225
+ path for every name carrying a title run (decisions/parse-cost).
228
226
  """
229
227
  return list(filter(None, map(_normalize, words)))
230
228
 
@@ -247,7 +245,7 @@ def _run_addresses_by_given(words: Iterable[str],
247
245
 
248
246
  Two sites ask it and they must agree -- post_rules for H1, group
249
247
  for the P5 licence -- because a run read two ways is a rule
250
- contradicting itself (decisions.md#P5, the 2026-08-22 #369 entry).
248
+ contradicting itself (decisions/P5, the 2026-08-22 #369 entry).
251
249
  One predicate, so they cannot drift.
252
250
 
253
251
  The WHOLE run's key, or that key's LAST word (#489): a run is
@@ -542,7 +540,7 @@ def _normpairs(
542
540
  # punctuation marks which letters are joined into one run
543
541
  # (_render._apply_mask reads that) and is never written into
544
542
  # the word. A raise, not a warning: a mismatched value used to
545
- # be SUBSTITUTED for the word, and rules.md#R4: "Repair
543
+ # be SUBSTITUTED for the word, and rules/R4: "Repair
546
544
  # changes case and nothing else", so there is no reading of
547
545
  # one that repair can honor.
548
546
  if _alnum(_normalize(v)) != _alnum(normalized_key):
@@ -582,8 +580,8 @@ class Lexicon:
582
580
  #: Pre-nominal titles ("dr", "sir", "capt", ...). Full default
583
581
  #: list: :data:`~nameparser.config.titles.TITLES`.
584
582
  titles: frozenset[str] = frozenset()
585
- #: Titles whose single following name reads as a GIVEN name
586
- #: ("sheikh", "sister", ...) rather than a family name. Full
583
+ #: Titles whose single name word, before or after them, reads as a
584
+ #: GIVEN name ("sheikh", "sister", ...) rather than a family name. Full
587
585
  #: default list: :data:`~nameparser.config.titles.GIVEN_NAME_TITLES`.
588
586
  given_name_titles: frozenset[str] = frozenset()
589
587
  #: Post-nominal acronym suffixes, matched with or without periods
@@ -599,7 +597,7 @@ class Lexicon:
599
597
  #: outright; written bare, capitals decide in a mixed-case name
600
598
  #: ("Jack MA" is the credential, "Jack Ma" keeps his family name)
601
599
  #: and otherwise the word counts as a suffix only where a name
602
- #: word is left beside it ("John Smith ma"), rules.md#S2. Full
600
+ #: word is left beside it ("John Smith ma"), rules/S6 and rules/S4. Full
603
601
  #: default list:
604
602
  #: :data:`~nameparser.config.suffixes.SUFFIX_ACRONYMS_AMBIGUOUS`.
605
603
  suffix_acronyms_ambiguous: frozenset[str] = frozenset()
@@ -644,8 +642,10 @@ class Lexicon:
644
642
  #: decided by the writing instead and never consults this set, and
645
643
  #: neither does a caseless letter (Arabic و), which has no case to
646
644
  #: read. A member additionally reports
647
- #: :attr:`~nameparser.AmbiguityKind.CONJUNCTION_OR_INITIAL`; a
648
- #: non-member reports nothing, its reading not being in doubt.
645
+ #: :attr:`~nameparser.AmbiguityKind.CONJUNCTION_OR_INITIAL` where a
646
+ #: name word of its part follows it (with none, it connects
647
+ #: nothing); a non-member reports nothing, its reading not being in
648
+ #: doubt.
649
649
  #: Full default list:
650
650
  #: :data:`~nameparser.config.conjunctions.CONJUNCTIONS_AMBIGUOUS`.
651
651
  #: Entries need not also be in ``conjunctions``; one that is not is
@@ -673,8 +673,8 @@ class Lexicon:
673
673
  #: (:data:`~nameparser.config.surnames.KOREAN_SURNAMES`); Chinese
674
674
  #: surnames ship in locales.ZH because Han segmentation is opt-in.
675
675
  surnames: frozenset[str] = frozenset()
676
- #: Honorifics that may be peeled off the END of a name token
677
- #: (#308), matched longest-first: 田中さん splits into 田中 and さん
676
+ #: Honorifics, and credential, office and generational words, that
677
+ #: may be peeled off the END of a name token (#308, #707), matched longest-first: 田中さん splits into 田中 and さん
678
678
  #: before the tokens are classified. Every entry must also be a
679
679
  #: :attr:`suffix_words` entry -- the peeled tail is claimed by
680
680
  #: suffix classification like any other post-nominal. Deliberately
@@ -690,6 +690,15 @@ class Lexicon:
690
690
  #: vocabulary is. Full default list:
691
691
  #: :data:`~nameparser.config.suffixes.GLUED_HONORIFICS`.
692
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()
693
702
  #: Lowercase word -> case mask used by capitalized(): the word's
694
703
  #: own letters and digits, each in the case it takes ("phd" ->
695
704
  #: "PhD"), laid over the word as written, so the one entry
@@ -737,9 +746,10 @@ class Lexicon:
737
746
  # the expensive one -- three working configurations broken
738
747
  # across two attempts. Do not add a third.
739
748
  #
740
- # The v2 form of particles.py's NON_GIVEN_NAME_PARTICLES-disjoint-
741
- # from-BOUND_GIVEN_NAMES assertion. That module guards its own
742
- # 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.
743
753
  contradictory = (
744
754
  self.bound_given_names & self.particles) - self.particles_ambiguous
745
755
  if contradictory:
@@ -945,7 +955,8 @@ def _default_lexicon() -> Lexicon:
945
955
  SUFFIX_WORDS,
946
956
  )
947
957
  from nameparser.config.surnames import KOREAN_SURNAMES
948
- from nameparser.config.titles import GIVEN_NAME_TITLES, TITLES
958
+ from nameparser.config.titles import (
959
+ AFTER_GIVEN_TITLES, GIVEN_NAME_TITLES, TITLES)
949
960
 
950
961
  # every vocabulary constant is a frozenset since #293, so each one
951
962
  # feeds its strictly-typed frozenset[str] field as it stands -- and
@@ -973,6 +984,7 @@ def _default_lexicon() -> Lexicon:
973
984
  maiden_markers=MAIDEN_MARKERS,
974
985
  surnames=KOREAN_SURNAMES,
975
986
  honorific_tails=GLUED_HONORIFICS,
987
+ after_given_titles=AFTER_GIVEN_TITLES,
976
988
  # pass canonical pair-tuples so this strictly-typed call site never
977
989
  # feeds a Mapping to the tuple-annotated field; __post_init__
978
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.md#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,12 +215,12 @@ 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
222
222
  # role, and a clear was measured and backed out --
223
- # decisions.md#C1 (2026-09-06 #511) carries the rest: the
223
+ # decisions/R1 (2026-09-06 #511) carries the rest: the
224
224
  # between-piece mark that rides onto a non-suffix role, and
225
225
  # which views read it. Dropped tokens keep their role: the
226
226
  # pass filters them by index and assemble omits them.
@@ -228,7 +228,7 @@ class Parser:
228
228
  tok if i in dropped
229
229
  else dataclasses.replace(tok, role=role)
230
230
  for i, tok in enumerate(state.tokens))
231
- # rules.md#R1: "a run of post-nominals written with spaces
231
+ # rules/R1: "a run of post-nominals written with spaces
232
232
  # renders with spaces, and one written with commas keeps
233
233
  # them" -- the same pass post_rules runs last, over the
234
234
  # value's own comma offsets, so the harvest carries exactly
@@ -346,13 +346,13 @@ def parser_for(*locales: Locale, base: Parser | None = None,
346
346
  # a subclass with extra mandatory args would break this rewrap
347
347
  raise type(exc)(
348
348
  f"while applying locale {loc.code!r}: {exc}") from exc
349
- # rules.md#D1: "constructing a parser that activates division for
349
+ # rules/D1: "constructing a parser that activates division for
350
350
  # scripts with no covering surnames and no segmenter warns at
351
351
  # construction, naming the dead scripts and each way out"
352
- # (history: decisions.md#D1)
353
- # rules.md#D2: "applying a locale pack wraps any such error with
352
+ # (history: decisions/D1)
353
+ # rules/D2: "applying a locale pack wraps any such error with
354
354
  # the locale's code, so a stacked configuration names which layer
355
- # broke" (history: decisions.md#D2)
355
+ # broke" (history: decisions/D2)
356
356
  # Construction warnings (the segmenterless-activation check in
357
357
  # Parser.__post_init__) re-emit from THIS frame: its stacklevel is
358
358
  # sized for direct Parser(...) construction, and through this
@@ -2,7 +2,8 @@
2
2
 
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
- #626).
5
+ #626), and segments, for the part a withdrawn connective report's
6
+ letter ends (rules/P8, #721).
6
7
  Produces: a validated ParsedName -- the constructor re-checks every
7
8
  invariant (span order/bounds, ambiguity subset), so a pipeline bug
8
9
  that would produce an invalid result dies HERE, not in a renderer
@@ -20,16 +21,16 @@ tests/v2/pipeline/test_assemble.py.
20
21
  """
21
22
  from __future__ import annotations
22
23
 
23
- from nameparser._pipeline._state import FIELD_READING, ParseState
24
+ from nameparser._pipeline._state import FIELD_READING, NAME_ROLES, ParseState
24
25
  from nameparser._types import (
25
26
  Ambiguity, AmbiguityKind, ParsedName, Role, Token,
26
27
  )
27
28
 
28
29
 
29
- # rules.md#A2: "a name with no name content parses to the empty
30
+ # rules/A2: "a name with no name content parses to the empty
30
31
  # name — every field empty, false as a boolean — while ambiguities
31
32
  # born from its punctuation survive on the empty result"
32
- # (history: decisions.md#A2)
33
+ # (history: decisions/A2)
33
34
  def assemble(state: ParseState) -> ParsedName:
34
35
  dropped = set(state.dropped)
35
36
  final: dict[int, Token] = {}
@@ -74,6 +75,7 @@ def assemble(state: ParseState) -> ParsedName:
74
75
  if contentless:
75
76
  final = {}
76
77
  ambiguities = []
78
+ segment_of: dict[int, tuple[int, ...]] | None = None
77
79
  for pending in state.ambiguities:
78
80
  materialized = tuple(final[i] for i in pending.indices
79
81
  if i in final)
@@ -85,7 +87,7 @@ def assemble(state: ParseState) -> ParsedName:
85
87
  # referents did not lose a contest, they were discarded
86
88
  # wholesale, and the report still describes the input.
87
89
  continue
88
- # rules.md#A1: "a report names the reading the parse took, so
90
+ # rules/A1: "a report names the reading the parse took, so
89
91
  # a report whose fork the rest of the parse then resolved to
90
92
  # NEITHER branch is withdrawn rather than carried beside a
91
93
  # reading it contradicts". classify's connective-or-initial
@@ -114,6 +116,23 @@ def assemble(state: ParseState) -> ParsedName:
114
116
  and any(t.role in (Role.SUFFIX, Role.TITLE)
115
117
  for t in materialized)):
116
118
  continue
119
+ # rules/P8: "unless no name word of its part stands to its
120
+ # right" -- nothing after the letter in its segment, or only
121
+ # suffixes, titles, a maiden clause or a nickname: it connects
122
+ # nothing, so the report goes (#721). Asked here for the reason
123
+ # above: the roles are final. Where the letter is also a
124
+ # numeral the fork read, its own report (rules/S10) is the one
125
+ # left, so one token never carries both. Built only for a
126
+ # connective report, so other names pay nothing.
127
+ if pending.kind is AmbiguityKind.CONJUNCTION_OR_INITIAL:
128
+ if segment_of is None:
129
+ segment_of = {j: seg for seg in state.segments
130
+ for j in seg}
131
+ i = pending.indices[0]
132
+ if not any(j > i and j in final
133
+ and final[j].role in NAME_ROLES
134
+ for j in segment_of.get(i, ())):
135
+ continue
117
136
  detail = pending.detail
118
137
  if pending.field_tail is not None:
119
138
  # A report naming a field, in a name emptied above (no