stride-align 0.6.0

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 (190) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/NOTICE +255 -0
  4. data/README.md +107 -0
  5. data/data/bmpm_data/gen_approx_any.txt +131 -0
  6. data/data/bmpm_data/gen_approx_arabic.txt +26 -0
  7. data/data/bmpm_data/gen_approx_common.txt +233 -0
  8. data/data/bmpm_data/gen_approx_cyrillic.txt +18 -0
  9. data/data/bmpm_data/gen_approx_czech.txt +18 -0
  10. data/data/bmpm_data/gen_approx_dutch.txt +18 -0
  11. data/data/bmpm_data/gen_approx_english.txt +47 -0
  12. data/data/bmpm_data/gen_approx_french.txt +25 -0
  13. data/data/bmpm_data/gen_approx_german.txt +73 -0
  14. data/data/bmpm_data/gen_approx_greek.txt +18 -0
  15. data/data/bmpm_data/gen_approx_greeklatin.txt +20 -0
  16. data/data/bmpm_data/gen_approx_hebrew.txt +18 -0
  17. data/data/bmpm_data/gen_approx_hungarian.txt +18 -0
  18. data/data/bmpm_data/gen_approx_italian.txt +18 -0
  19. data/data/bmpm_data/gen_approx_polish.txt +84 -0
  20. data/data/bmpm_data/gen_approx_portuguese.txt +18 -0
  21. data/data/bmpm_data/gen_approx_romanian.txt +18 -0
  22. data/data/bmpm_data/gen_approx_russian.txt +48 -0
  23. data/data/bmpm_data/gen_approx_spanish.txt +21 -0
  24. data/data/bmpm_data/gen_approx_turkish.txt +18 -0
  25. data/data/bmpm_data/gen_exact_any.txt +40 -0
  26. data/data/bmpm_data/gen_exact_approx_common.txt +79 -0
  27. data/data/bmpm_data/gen_exact_arabic.txt +18 -0
  28. data/data/bmpm_data/gen_exact_common.txt +32 -0
  29. data/data/bmpm_data/gen_exact_cyrillic.txt +18 -0
  30. data/data/bmpm_data/gen_exact_czech.txt +18 -0
  31. data/data/bmpm_data/gen_exact_dutch.txt +18 -0
  32. data/data/bmpm_data/gen_exact_english.txt +18 -0
  33. data/data/bmpm_data/gen_exact_french.txt +18 -0
  34. data/data/bmpm_data/gen_exact_german.txt +18 -0
  35. data/data/bmpm_data/gen_exact_greek.txt +18 -0
  36. data/data/bmpm_data/gen_exact_greeklatin.txt +18 -0
  37. data/data/bmpm_data/gen_exact_hebrew.txt +18 -0
  38. data/data/bmpm_data/gen_exact_hungarian.txt +18 -0
  39. data/data/bmpm_data/gen_exact_italian.txt +18 -0
  40. data/data/bmpm_data/gen_exact_polish.txt +23 -0
  41. data/data/bmpm_data/gen_exact_portuguese.txt +18 -0
  42. data/data/bmpm_data/gen_exact_romanian.txt +18 -0
  43. data/data/bmpm_data/gen_exact_russian.txt +19 -0
  44. data/data/bmpm_data/gen_exact_spanish.txt +19 -0
  45. data/data/bmpm_data/gen_exact_turkish.txt +18 -0
  46. data/data/bmpm_data/gen_hebrew_common.txt +113 -0
  47. data/data/bmpm_data/gen_lang.txt +295 -0
  48. data/data/bmpm_data/gen_languages.txt +36 -0
  49. data/data/bmpm_data/gen_rules_any.txt +367 -0
  50. data/data/bmpm_data/gen_rules_arabic.txt +76 -0
  51. data/data/bmpm_data/gen_rules_cyrillic.txt +99 -0
  52. data/data/bmpm_data/gen_rules_czech.txt +67 -0
  53. data/data/bmpm_data/gen_rules_dutch.txt +78 -0
  54. data/data/bmpm_data/gen_rules_english.txt +113 -0
  55. data/data/bmpm_data/gen_rules_french.txt +114 -0
  56. data/data/bmpm_data/gen_rules_german.txt +129 -0
  57. data/data/bmpm_data/gen_rules_greek.txt +97 -0
  58. data/data/bmpm_data/gen_rules_greeklatin.txt +118 -0
  59. data/data/bmpm_data/gen_rules_hebrew.txt +62 -0
  60. data/data/bmpm_data/gen_rules_hungarian.txt +83 -0
  61. data/data/bmpm_data/gen_rules_italian.txt +77 -0
  62. data/data/bmpm_data/gen_rules_polish.txt +185 -0
  63. data/data/bmpm_data/gen_rules_portuguese.txt +105 -0
  64. data/data/bmpm_data/gen_rules_romanian.txt +64 -0
  65. data/data/bmpm_data/gen_rules_russian.txt +142 -0
  66. data/data/bmpm_data/gen_rules_spanish.txt +85 -0
  67. data/data/bmpm_data/gen_rules_turkish.txt +50 -0
  68. data/data/keyboard_data/qwerty.npy +0 -0
  69. data/data/matrix_data/BLOSUM100 +31 -0
  70. data/data/matrix_data/BLOSUM30 +31 -0
  71. data/data/matrix_data/BLOSUM35 +31 -0
  72. data/data/matrix_data/BLOSUM40 +31 -0
  73. data/data/matrix_data/BLOSUM45 +25 -0
  74. data/data/matrix_data/BLOSUM50 +25 -0
  75. data/data/matrix_data/BLOSUM55 +31 -0
  76. data/data/matrix_data/BLOSUM60 +31 -0
  77. data/data/matrix_data/BLOSUM62 +25 -0
  78. data/data/matrix_data/BLOSUM65 +31 -0
  79. data/data/matrix_data/BLOSUM70 +31 -0
  80. data/data/matrix_data/BLOSUM75 +31 -0
  81. data/data/matrix_data/BLOSUM80 +25 -0
  82. data/data/matrix_data/BLOSUM85 +31 -0
  83. data/data/matrix_data/BLOSUM90 +25 -0
  84. data/data/matrix_data/NUC.4.4 +25 -0
  85. data/data/matrix_data/PAM10 +34 -0
  86. data/data/matrix_data/PAM100 +34 -0
  87. data/data/matrix_data/PAM110 +34 -0
  88. data/data/matrix_data/PAM120 +34 -0
  89. data/data/matrix_data/PAM130 +34 -0
  90. data/data/matrix_data/PAM140 +34 -0
  91. data/data/matrix_data/PAM150 +34 -0
  92. data/data/matrix_data/PAM160 +34 -0
  93. data/data/matrix_data/PAM170 +34 -0
  94. data/data/matrix_data/PAM180 +34 -0
  95. data/data/matrix_data/PAM190 +34 -0
  96. data/data/matrix_data/PAM20 +34 -0
  97. data/data/matrix_data/PAM200 +34 -0
  98. data/data/matrix_data/PAM210 +34 -0
  99. data/data/matrix_data/PAM220 +34 -0
  100. data/data/matrix_data/PAM230 +34 -0
  101. data/data/matrix_data/PAM240 +34 -0
  102. data/data/matrix_data/PAM250 +25 -0
  103. data/data/matrix_data/PAM260 +34 -0
  104. data/data/matrix_data/PAM270 +34 -0
  105. data/data/matrix_data/PAM280 +34 -0
  106. data/data/matrix_data/PAM290 +34 -0
  107. data/data/matrix_data/PAM30 +25 -0
  108. data/data/matrix_data/PAM300 +34 -0
  109. data/data/matrix_data/PAM310 +34 -0
  110. data/data/matrix_data/PAM320 +34 -0
  111. data/data/matrix_data/PAM330 +34 -0
  112. data/data/matrix_data/PAM340 +34 -0
  113. data/data/matrix_data/PAM350 +34 -0
  114. data/data/matrix_data/PAM360 +34 -0
  115. data/data/matrix_data/PAM370 +34 -0
  116. data/data/matrix_data/PAM380 +34 -0
  117. data/data/matrix_data/PAM390 +34 -0
  118. data/data/matrix_data/PAM40 +34 -0
  119. data/data/matrix_data/PAM400 +34 -0
  120. data/data/matrix_data/PAM410 +34 -0
  121. data/data/matrix_data/PAM420 +34 -0
  122. data/data/matrix_data/PAM430 +34 -0
  123. data/data/matrix_data/PAM440 +34 -0
  124. data/data/matrix_data/PAM450 +34 -0
  125. data/data/matrix_data/PAM460 +34 -0
  126. data/data/matrix_data/PAM470 +34 -0
  127. data/data/matrix_data/PAM480 +34 -0
  128. data/data/matrix_data/PAM490 +34 -0
  129. data/data/matrix_data/PAM50 +34 -0
  130. data/data/matrix_data/PAM500 +34 -0
  131. data/data/matrix_data/PAM60 +34 -0
  132. data/data/matrix_data/PAM70 +25 -0
  133. data/data/matrix_data/PAM80 +34 -0
  134. data/data/matrix_data/PAM90 +34 -0
  135. data/ext/stride_align/backend_avx2.cpp +2 -0
  136. data/ext/stride_align/backend_avx512bwvl.cpp +2 -0
  137. data/ext/stride_align/backend_generic.cpp +3 -0
  138. data/ext/stride_align/backend_impl.hpp +983 -0
  139. data/ext/stride_align/backend_lasx.cpp +2 -0
  140. data/ext/stride_align/backend_lsx.cpp +2 -0
  141. data/ext/stride_align/backend_neon.cpp +2 -0
  142. data/ext/stride_align/backend_rvv.cpp +2 -0
  143. data/ext/stride_align/backend_sse41.cpp +2 -0
  144. data/ext/stride_align/backend_sve.cpp +2 -0
  145. data/ext/stride_align/backend_sve2.cpp +2 -0
  146. data/ext/stride_align/backend_vsx.cpp +2 -0
  147. data/ext/stride_align/beider_morse_impl.cpp +5 -0
  148. data/ext/stride_align/cpu_detect.cpp +172 -0
  149. data/ext/stride_align/cpu_detect.hpp +6 -0
  150. data/ext/stride_align/extconf.rb +114 -0
  151. data/ext/stride_align/target_profile.hpp +82 -0
  152. data/ext/stride_align/vendor/beider_morse_impl.cpp +1467 -0
  153. data/ext/stride_align/vendor/stride_align/alignment.hpp +199 -0
  154. data/ext/stride_align/vendor/stride_align/batch.hpp +812 -0
  155. data/ext/stride_align/vendor/stride_align/beider_morse.hpp +121 -0
  156. data/ext/stride_align/vendor/stride_align/caverphone.hpp +222 -0
  157. data/ext/stride_align/vendor/stride_align/cologne_phonetic.hpp +202 -0
  158. data/ext/stride_align/vendor/stride_align/core.hpp +731 -0
  159. data/ext/stride_align/vendor/stride_align/daitch_mokotoff.hpp +631 -0
  160. data/ext/stride_align/vendor/stride_align/double_metaphone.hpp +796 -0
  161. data/ext/stride_align/vendor/stride_align/dtw.hpp +300 -0
  162. data/ext/stride_align/vendor/stride_align/encoded.hpp +235 -0
  163. data/ext/stride_align/vendor/stride_align/hamming.hpp +55 -0
  164. data/ext/stride_align/vendor/stride_align/indel.hpp +1200 -0
  165. data/ext/stride_align/vendor/stride_align/jaro.hpp +517 -0
  166. data/ext/stride_align/vendor/stride_align/lcs.hpp +159 -0
  167. data/ext/stride_align/vendor/stride_align/levenshtein.hpp +1247 -0
  168. data/ext/stride_align/vendor/stride_align/levenshtein_prepared.hpp +193 -0
  169. data/ext/stride_align/vendor/stride_align/match_rating.hpp +168 -0
  170. data/ext/stride_align/vendor/stride_align/metaphone.hpp +291 -0
  171. data/ext/stride_align/vendor/stride_align/ngram.hpp +176 -0
  172. data/ext/stride_align/vendor/stride_align/nysiis.hpp +199 -0
  173. data/ext/stride_align/vendor/stride_align/pairwise_alignment.hpp +465 -0
  174. data/ext/stride_align/vendor/stride_align/partial_ratio.hpp +486 -0
  175. data/ext/stride_align/vendor/stride_align/ratcliff_obershelp.hpp +101 -0
  176. data/ext/stride_align/vendor/stride_align/soundex.hpp +108 -0
  177. data/ext/stride_align/vendor/stride_align/token_ratios.hpp +445 -0
  178. data/ext/stride_align/vendor/stride_align/types.hpp +16 -0
  179. data/ext/stride_align/vendor/stride_align/utf8.hpp +512 -0
  180. data/ext/stride_align/vendor/stride_align/wratio.hpp +363 -0
  181. data/lib/stride_align/algorithms.rb +296 -0
  182. data/lib/stride_align/alignment_path.rb +217 -0
  183. data/lib/stride_align/backend.rb +47 -0
  184. data/lib/stride_align/batch.rb +705 -0
  185. data/lib/stride_align/core.rb +180 -0
  186. data/lib/stride_align/keyboard.rb +200 -0
  187. data/lib/stride_align/matrices.rb +403 -0
  188. data/lib/stride_align/version.rb +5 -0
  189. data/lib/stride_align.rb +87 -0
  190. metadata +231 -0
@@ -0,0 +1,631 @@
1
+ #pragma once
2
+
3
+ // Daitch-Mokotoff Soundex (Daitch & Mokotoff, 1985).
4
+ //
5
+ // A refinement of American Soundex tuned for Slavic and Yiddish
6
+ // surnames: 6-digit output (vs Soundex's letter-and-three-digits),
7
+ // the leading letter is encoded (not preserved verbatim), and rules
8
+ // fire on multi-character clusters (``sch``, ``tsch``, ``schtsch``,
9
+ // ``rz``, ``cz``) before any single-letter fallback. Several rules
10
+ // emit branched alternatives separated by ``|``; the returned string
11
+ // is a ``|``-joined set of code candidates.
12
+ //
13
+ // Public API:
14
+ // * ``daitch_mokotoff(s)`` — branching enabled, returns
15
+ // ``code1|code2|...``.
16
+ // * ``daitch_mokotoff(s, branching=false)`` — single-code form.
17
+ // * Optional ``folding=true`` (default) applies the ASCII fold table
18
+ // so common accented characters (è/é/ê/ë/ï/ö/ü/ß ...) collapse to
19
+ // their ASCII bases before encoding.
20
+ //
21
+ // Source attribution:
22
+ // * Daitch, R. & Mokotoff, G. (1985). Original publication via
23
+ // Avotaynu Inc.
24
+ // * Apache Commons Codec ``DaitchMokotoffSoundex`` (Apache 2.0) and
25
+ // its companion resource file ``dmrules.txt`` for the rule table
26
+ // and algorithm. The rule data is vendored verbatim under
27
+ // ``docs/upstream/apache-commons-codec-dmrules.txt`` with its
28
+ // original ASF header intact and embedded below as the
29
+ // ``kRules`` raw string literal.
30
+ // * https://www.avotaynu.com/soundex.htm
31
+ // * https://en.wikipedia.org/wiki/Daitch-Mokotoff_Soundex
32
+ //
33
+ // Cross-checked against the canonical ``DaitchMokotoffSoundexTest``
34
+ // vectors (Apache Commons Codec).
35
+
36
+ #include <algorithm>
37
+ #include <array>
38
+ #include <cstdint>
39
+ #include <cstring>
40
+ #include <mutex>
41
+ #include <string>
42
+ #include <string_view>
43
+ #include <unordered_map>
44
+ #include <vector>
45
+
46
+ namespace stride_align::phonetic {
47
+
48
+ namespace daitch_mokotoff_detail {
49
+
50
+ constexpr std::size_t kMaxLength = 6;
51
+
52
+ // Rule data — vendored verbatim from
53
+ // ``docs/upstream/apache-commons-codec-dmrules.txt`` (Apache 2.0,
54
+ // Apache Commons Codec). The ASF header from that file is reproduced
55
+ // inside the raw string so the derivative-work attribution survives
56
+ // even if this single header is read in isolation.
57
+ inline constexpr std::string_view kRules = R"DM(
58
+ /*
59
+ * Licensed to the Apache Software Foundation (ASF) under one or more
60
+ * contributor license agreements. See the NOTICE file distributed with
61
+ * this work for additional information regarding copyright ownership.
62
+ * The ASF licenses this file to You under the Apache License, Version 2.0
63
+ * (the "License"); you may not use this file except in compliance with
64
+ * the License. You may obtain a copy of the License at
65
+ *
66
+ * http://www.apache.org/licenses/LICENSE-2.0
67
+ *
68
+ * Unless required by applicable law or agreed to in writing, software
69
+ * distributed under the License is distributed on an "AS IS" BASIS,
70
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
71
+ * See the License for the specific language governing permissions and
72
+ * limitations under the License.
73
+ */
74
+
75
+ // Vowels
76
+ "a" "0" "" ""
77
+ "e" "0" "" ""
78
+ "i" "0" "" ""
79
+ "o" "0" "" ""
80
+ "u" "0" "" ""
81
+
82
+ // Consonants
83
+ "b" "7" "7" "7"
84
+ "d" "3" "3" "3"
85
+ "f" "7" "7" "7"
86
+ "g" "5" "5" "5"
87
+ "h" "5" "5" ""
88
+ "k" "5" "5" "5"
89
+ "l" "8" "8" "8"
90
+ "m" "6" "6" "6"
91
+ "n" "6" "6" "6"
92
+ "p" "7" "7" "7"
93
+ "q" "5" "5" "5"
94
+ "r" "9" "9" "9"
95
+ "s" "4" "4" "4"
96
+ "t" "3" "3" "3"
97
+ "v" "7" "7" "7"
98
+ "w" "7" "7" "7"
99
+ "x" "5" "54" "54"
100
+ "y" "1" "" ""
101
+ "z" "4" "4" "4"
102
+
103
+ "ţ" "3|4" "3|4" "3|4"
104
+ "ț" "3|4" "3|4" "3|4"
105
+
106
+ "ę" "" "" "|6"
107
+ "ą" "" "" "|6"
108
+
109
+ "schtsch" "2" "4" "4"
110
+ "schtsh" "2" "4" "4"
111
+ "schtch" "2" "4" "4"
112
+ "shtch" "2" "4" "4"
113
+ "shtsh" "2" "4" "4"
114
+ "stsch" "2" "4" "4"
115
+ "ttsch" "4" "4" "4"
116
+ "zhdzh" "2" "4" "4"
117
+ "shch" "2" "4" "4"
118
+ "scht" "2" "43" "43"
119
+ "schd" "2" "43" "43"
120
+ "stch" "2" "4" "4"
121
+ "strz" "2" "4" "4"
122
+ "strs" "2" "4" "4"
123
+ "stsh" "2" "4" "4"
124
+ "szcz" "2" "4" "4"
125
+ "szcs" "2" "4" "4"
126
+ "ttch" "4" "4" "4"
127
+ "tsch" "4" "4" "4"
128
+ "ttsz" "4" "4" "4"
129
+ "zdzh" "2" "4" "4"
130
+ "zsch" "4" "4" "4"
131
+ "chs" "5" "54" "54"
132
+ "csz" "4" "4" "4"
133
+ "czs" "4" "4" "4"
134
+ "drz" "4" "4" "4"
135
+ "drs" "4" "4" "4"
136
+ "dsh" "4" "4" "4"
137
+ "dsz" "4" "4" "4"
138
+ "dzh" "4" "4" "4"
139
+ "dzs" "4" "4" "4"
140
+ "sch" "4" "4" "4"
141
+ "sht" "2" "43" "43"
142
+ "szt" "2" "43" "43"
143
+ "shd" "2" "43" "43"
144
+ "szd" "2" "43" "43"
145
+ "tch" "4" "4" "4"
146
+ "trz" "4" "4" "4"
147
+ "trs" "4" "4" "4"
148
+ "tsh" "4" "4" "4"
149
+ "tts" "4" "4" "4"
150
+ "ttz" "4" "4" "4"
151
+ "tzs" "4" "4" "4"
152
+ "tsz" "4" "4" "4"
153
+ "zdz" "2" "4" "4"
154
+ "zhd" "2" "43" "43"
155
+ "zsh" "4" "4" "4"
156
+ "ai" "0" "1" ""
157
+ "aj" "0" "1" ""
158
+ "ay" "0" "1" ""
159
+ "au" "0" "7" ""
160
+ "cz" "4" "4" "4"
161
+ "cs" "4" "4" "4"
162
+ "ds" "4" "4" "4"
163
+ "dz" "4" "4" "4"
164
+ "dt" "3" "3" "3"
165
+ "ei" "0" "1" ""
166
+ "ej" "0" "1" ""
167
+ "ey" "0" "1" ""
168
+ "eu" "1" "1" ""
169
+ "fb" "7" "7" "7"
170
+ "ia" "1" "" ""
171
+ "ie" "1" "" ""
172
+ "io" "1" "" ""
173
+ "iu" "1" "" ""
174
+ "ks" "5" "54" "54"
175
+ "kh" "5" "5" "5"
176
+ "mn" "66" "66" "66"
177
+ "nm" "66" "66" "66"
178
+ "oi" "0" "1" ""
179
+ "oj" "0" "1" ""
180
+ "oy" "0" "1" ""
181
+ "pf" "7" "7" "7"
182
+ "ph" "7" "7" "7"
183
+ "sh" "4" "4" "4"
184
+ "sc" "2" "4" "4"
185
+ "st" "2" "43" "43"
186
+ "sd" "2" "43" "43"
187
+ "sz" "4" "4" "4"
188
+ "th" "3" "3" "3"
189
+ "ts" "4" "4" "4"
190
+ "tc" "4" "4" "4"
191
+ "tz" "4" "4" "4"
192
+ "ui" "0" "1" ""
193
+ "uj" "0" "1" ""
194
+ "uy" "0" "1" ""
195
+ "ue" "0" "1" ""
196
+ "zd" "2" "43" "43"
197
+ "zh" "4" "4" "4"
198
+ "zs" "4" "4" "4"
199
+
200
+ "c" "4|5" "4|5" "4|5"
201
+ "ch" "4|5" "4|5" "4|5"
202
+ "ck" "5|45" "5|45" "5|45"
203
+ "rs" "4|94" "4|94" "4|94"
204
+ "rz" "4|94" "4|94" "4|94"
205
+ "j" "1|4" "|4" "|4"
206
+
207
+ ß=s
208
+ à=a
209
+ á=a
210
+ â=a
211
+ ã=a
212
+ ä=a
213
+ å=a
214
+ æ=a
215
+ ç=c
216
+ è=e
217
+ é=e
218
+ ê=e
219
+ ë=e
220
+ ì=i
221
+ í=i
222
+ î=i
223
+ ï=i
224
+ ð=d
225
+ ñ=n
226
+ ò=o
227
+ ó=o
228
+ ô=o
229
+ õ=o
230
+ ö=o
231
+ ø=o
232
+ ù=u
233
+ ú=u
234
+ û=u
235
+ ý=y
236
+ þ=b
237
+ ÿ=y
238
+ ć=c
239
+ ł=l
240
+ ś=s
241
+ ż=z
242
+ ź=z
243
+ )DM";
244
+
245
+ // One rule row. ``pattern`` is a UTF-8 byte string (1–7 bytes for the
246
+ // Romanian / Polish entries). ``r_start`` / ``r_vowel`` / ``r_other``
247
+ // hold ``|``-separated alternative replacements; an empty alternative
248
+ // is a valid value (silent in that context).
249
+ struct Rule {
250
+ std::string pattern;
251
+ std::vector<std::string> r_start;
252
+ std::vector<std::string> r_vowel;
253
+ std::vector<std::string> r_other;
254
+ };
255
+
256
+ // Forward declaration — defined further down. Needed here so the
257
+ // folding-table parser can decode the accented ``<from>`` side of
258
+ // each folding line to a codepoint key.
259
+ inline std::uint32_t decode_one(std::string_view s, std::size_t pos,
260
+ std::size_t& consumed);
261
+
262
+ struct CompiledTables {
263
+ // Rules grouped by the first byte of their pattern; within each
264
+ // bucket sorted by descending pattern length so longest-match wins.
265
+ std::array<std::vector<Rule>, 256> by_first_byte;
266
+ // ASCII folding: maps a single codepoint to a single ASCII byte.
267
+ // Keying directly on the codepoint means ``cleanup`` can look up
268
+ // the fold result without encoding the codepoint to UTF-8 just to
269
+ // build the search key.
270
+ std::unordered_map<std::uint32_t, char> foldings;
271
+ };
272
+
273
+ inline std::vector<std::string> split_alts(std::string_view s) {
274
+ std::vector<std::string> out;
275
+ std::size_t start = 0;
276
+ for (std::size_t i = 0; i <= s.size(); ++i) {
277
+ if (i == s.size() || s[i] == '|') {
278
+ out.emplace_back(s.substr(start, i - start));
279
+ start = i + 1;
280
+ }
281
+ }
282
+ return out;
283
+ }
284
+
285
+ inline std::string_view trim(std::string_view s) {
286
+ while (!s.empty() && (s.front() == ' ' || s.front() == '\t' || s.front() == '\r')) {
287
+ s.remove_prefix(1);
288
+ }
289
+ while (!s.empty() && (s.back() == ' ' || s.back() == '\t' || s.back() == '\r')) {
290
+ s.remove_suffix(1);
291
+ }
292
+ return s;
293
+ }
294
+
295
+ // Tokenise a rule line: whitespace-split, then strip exactly one
296
+ // leading and one trailing ``"`` per token (mirrors Apache Commons
297
+ // Codec ``DaitchMokotoffSoundex.stripQuotes``).
298
+ inline std::vector<std::string> tokenise(std::string_view line) {
299
+ std::vector<std::string> out;
300
+ std::size_t i = 0;
301
+ while (i < line.size()) {
302
+ while (i < line.size() && (line[i] == ' ' || line[i] == '\t')) ++i;
303
+ if (i >= line.size()) break;
304
+ const std::size_t start = i;
305
+ while (i < line.size() && line[i] != ' ' && line[i] != '\t') ++i;
306
+ std::string tok(line.substr(start, i - start));
307
+ if (!tok.empty() && tok.front() == '"') tok.erase(0, 1);
308
+ if (!tok.empty() && tok.back() == '"') tok.pop_back();
309
+ out.push_back(std::move(tok));
310
+ }
311
+ return out;
312
+ }
313
+
314
+ inline CompiledTables parse_rules() {
315
+ CompiledTables t;
316
+ std::string_view src = kRules;
317
+ bool in_block = false;
318
+ std::size_t i = 0;
319
+ while (i < src.size()) {
320
+ std::size_t end = src.find('\n', i);
321
+ if (end == std::string_view::npos) end = src.size();
322
+ std::string_view line = src.substr(i, end - i);
323
+ i = end + 1;
324
+
325
+ if (in_block) {
326
+ if (line.find("*/") != std::string_view::npos) in_block = false;
327
+ continue;
328
+ }
329
+ auto trimmed = trim(line);
330
+ if (trimmed.size() >= 2 && trimmed[0] == '/' && trimmed[1] == '*') {
331
+ // single- or multi-line block comment
332
+ if (trimmed.find("*/") == std::string_view::npos) in_block = true;
333
+ continue;
334
+ }
335
+ // strip ``// ...`` end-of-line comment
336
+ auto cmt = trimmed.find("//");
337
+ if (cmt != std::string_view::npos) trimmed = trimmed.substr(0, cmt);
338
+ trimmed = trim(trimmed);
339
+ if (trimmed.empty()) continue;
340
+
341
+ if (trimmed.find('=') != std::string_view::npos) {
342
+ // folding: ``<accented>=<ascii>`` where ``<accented>`` is a single
343
+ // codepoint encoded in the rule file as UTF-8 — decode it to a
344
+ // codepoint key at parse time.
345
+ const auto eq = trimmed.find('=');
346
+ const auto from = trimmed.substr(0, eq);
347
+ const auto to = trimmed.substr(eq + 1);
348
+ if (!from.empty() && to.size() == 1) {
349
+ std::size_t consumed = 0;
350
+ const std::uint32_t cp = decode_one(from, 0, consumed);
351
+ if (consumed == from.size()) {
352
+ t.foldings.emplace(cp, to.front());
353
+ }
354
+ }
355
+ continue;
356
+ }
357
+
358
+ const auto parts = tokenise(trimmed);
359
+ if (parts.size() != 4) continue;
360
+ Rule r;
361
+ r.pattern = parts[0];
362
+ r.r_start = split_alts(parts[1]);
363
+ r.r_vowel = split_alts(parts[2]);
364
+ r.r_other = split_alts(parts[3]);
365
+ if (r.pattern.empty()) continue;
366
+ const auto first = static_cast<unsigned char>(r.pattern.front());
367
+ t.by_first_byte[first].push_back(std::move(r));
368
+ }
369
+ for (auto& bucket : t.by_first_byte) {
370
+ std::stable_sort(bucket.begin(), bucket.end(),
371
+ [](const Rule& a, const Rule& b) {
372
+ return a.pattern.size() > b.pattern.size();
373
+ });
374
+ }
375
+ return t;
376
+ }
377
+
378
+ inline const CompiledTables& tables() {
379
+ static const CompiledTables t = parse_rules();
380
+ return t;
381
+ }
382
+
383
+ // Lower-case ASCII fold, mirroring Apache Commons Codec
384
+ // ``Character.toLowerCase`` for the ASCII range.
385
+ inline char to_lower_ascii(char c) {
386
+ return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - ('A' - 'A') + 32) : c;
387
+ }
388
+
389
+ inline bool is_ascii_letter(char c) {
390
+ return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
391
+ }
392
+
393
+ // Decode the next UTF-8 codepoint at ``pos``. Returns the codepoint
394
+ // and updates ``consumed`` with the byte count; on malformed input
395
+ // consumes one byte and returns the raw byte value (so non-letter
396
+ // junk is silently dropped by the caller's letter check).
397
+ inline std::uint32_t decode_one(std::string_view s, std::size_t pos,
398
+ std::size_t& consumed) {
399
+ const auto b0 = static_cast<std::uint8_t>(s[pos]);
400
+ if (b0 < 0x80) { consumed = 1; return b0; }
401
+ if ((b0 & 0xE0) == 0xC0 && pos + 1 < s.size()) {
402
+ consumed = 2;
403
+ return ((b0 & 0x1F) << 6) |
404
+ (static_cast<std::uint8_t>(s[pos + 1]) & 0x3F);
405
+ }
406
+ if ((b0 & 0xF0) == 0xE0 && pos + 2 < s.size()) {
407
+ consumed = 3;
408
+ return ((b0 & 0x0F) << 12) |
409
+ ((static_cast<std::uint8_t>(s[pos + 1]) & 0x3F) << 6) |
410
+ (static_cast<std::uint8_t>(s[pos + 2]) & 0x3F);
411
+ }
412
+ if ((b0 & 0xF8) == 0xF0 && pos + 3 < s.size()) {
413
+ consumed = 4;
414
+ return ((b0 & 0x07) << 18) |
415
+ ((static_cast<std::uint8_t>(s[pos + 1]) & 0x3F) << 12) |
416
+ ((static_cast<std::uint8_t>(s[pos + 2]) & 0x3F) << 6) |
417
+ (static_cast<std::uint8_t>(s[pos + 3]) & 0x3F);
418
+ }
419
+ consumed = 1;
420
+ return b0;
421
+ }
422
+
423
+ inline bool is_letter_cp(std::uint32_t cp) {
424
+ // ASCII letters first; then a permissive sweep over the codepoints
425
+ // the upstream rule / folding files care about. Anything not in
426
+ // either set is treated as non-letter and dropped during cleanup —
427
+ // matches Apache Commons Codec's ``Character.isLetter`` for the
428
+ // surnames this encoder targets.
429
+ if ((cp >= 'A' && cp <= 'Z') || (cp >= 'a' && cp <= 'z')) return true;
430
+ return cp >= 0x00C0; // Latin Supplement and beyond
431
+ }
432
+
433
+ inline bool is_whitespace_cp(std::uint32_t cp) {
434
+ return cp == ' ' || cp == '\t' || cp == '\n' || cp == '\r';
435
+ }
436
+
437
+ // To-lower for Latin-1 / Latin-Extended-A characters that the
438
+ // folding table keys are lower-case for already. Returns the new
439
+ // codepoint.
440
+ inline std::uint32_t lower_cp(std::uint32_t cp) {
441
+ if (cp >= 'A' && cp <= 'Z') return cp + 32;
442
+ if (cp >= 0x00C0 && cp <= 0x00DE && cp != 0x00D7) return cp + 32;
443
+ // Latin-Extended-A even codepoints upper, odd lower (mostly).
444
+ if (cp >= 0x0100 && cp <= 0x017F && (cp & 1) == 0) return cp + 1;
445
+ return cp;
446
+ }
447
+
448
+ // Encode one codepoint as UTF-8 onto ``out``. Pure helper, no state.
449
+ inline void append_cp_as_utf8(std::string& out, std::uint32_t cp) {
450
+ if (cp < 0x80) {
451
+ out.push_back(static_cast<char>(cp));
452
+ } else if (cp < 0x800) {
453
+ out.push_back(static_cast<char>(0xC0 | (cp >> 6)));
454
+ out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
455
+ } else if (cp < 0x10000) {
456
+ out.push_back(static_cast<char>(0xE0 | (cp >> 12)));
457
+ out.push_back(static_cast<char>(0x80 | ((cp >> 6) & 0x3F)));
458
+ out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
459
+ } else {
460
+ out.push_back(static_cast<char>(0xF0 | (cp >> 18)));
461
+ out.push_back(static_cast<char>(0x80 | ((cp >> 12) & 0x3F)));
462
+ out.push_back(static_cast<char>(0x80 | ((cp >> 6) & 0x3F)));
463
+ out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
464
+ }
465
+ }
466
+
467
+ // Cleanup takes the codepoint stream the dispatch layer already
468
+ // produced (no UTF-8 round-trip on the input side). The output is a
469
+ // UTF-8 byte string because the rule matching loop is byte-level —
470
+ // it walks ``cleaned[pos]`` looking up the per-first-byte bucket.
471
+ // Folding turns each codepoint in the fold table into a single ASCII
472
+ // byte; unfolded codepoints stay as their natural UTF-8 encoding so
473
+ // the few non-ASCII rule patterns (``ţ``, ``ț``, ``ę``, ``ą``)
474
+ // match.
475
+ inline std::string cleanup(const std::vector<std::uint32_t>& input,
476
+ bool folding) {
477
+ const auto& t = tables();
478
+ std::string out;
479
+ out.reserve(input.size());
480
+ for (auto cp : input) {
481
+ if (is_whitespace_cp(cp) || !is_letter_cp(cp)) continue;
482
+ cp = lower_cp(cp);
483
+ if (folding) {
484
+ auto it = t.foldings.find(cp);
485
+ if (it != t.foldings.end()) {
486
+ out.push_back(it->second);
487
+ continue;
488
+ }
489
+ }
490
+ append_cp_as_utf8(out, cp);
491
+ }
492
+ return out;
493
+ }
494
+
495
+ // Returns true if the byte at ``pos`` in ``s`` is an ASCII vowel.
496
+ // The encoder peels accents via folding before rule application, so
497
+ // the only vowels that reach the next-char check are ASCII.
498
+ inline bool is_ascii_vowel(char c) {
499
+ return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u';
500
+ }
501
+
502
+ struct Branch {
503
+ std::string text;
504
+ std::string last_replacement;
505
+ };
506
+
507
+ inline void process_replacement(Branch& b, const std::string& replacement,
508
+ bool force_append) {
509
+ const bool append = b.last_replacement.empty() ||
510
+ !b.last_replacement.ends_with(replacement) ||
511
+ force_append;
512
+ if (append && b.text.size() < kMaxLength) {
513
+ b.text.append(replacement);
514
+ if (b.text.size() > kMaxLength) b.text.resize(kMaxLength);
515
+ }
516
+ b.last_replacement = replacement;
517
+ }
518
+
519
+ inline std::string daitch_mokotoff_impl(const std::vector<std::uint32_t>& input,
520
+ bool branching, bool folding) {
521
+ const auto& t = tables();
522
+ const std::string cleaned = cleanup(input, folding);
523
+ std::vector<Branch> current;
524
+ current.push_back({}); // single empty branch
525
+
526
+ char last_char = '\0';
527
+ std::size_t pos = 0;
528
+ while (pos < cleaned.size()) {
529
+ const auto first = static_cast<unsigned char>(cleaned[pos]);
530
+ const auto& bucket = t.by_first_byte[first];
531
+ if (bucket.empty()) {
532
+ // Stay on this position one byte; matches Apache Commons Codec's
533
+ // behaviour of advancing one char on no-rule (it then re-loops
534
+ // — net effect is the same as skip).
535
+ ++pos;
536
+ continue;
537
+ }
538
+ bool matched = false;
539
+ for (const auto& rule : bucket) {
540
+ if (rule.pattern.size() > cleaned.size() - pos) continue;
541
+ if (std::memcmp(cleaned.data() + pos, rule.pattern.data(),
542
+ rule.pattern.size()) != 0) {
543
+ continue;
544
+ }
545
+ // Pick the replacement column.
546
+ const std::size_t next_index = pos + rule.pattern.size();
547
+ const bool at_start = (last_char == '\0');
548
+ const std::vector<std::string>* reps;
549
+ if (at_start) {
550
+ reps = &rule.r_start;
551
+ } else if (next_index < cleaned.size() &&
552
+ is_ascii_vowel(cleaned[next_index])) {
553
+ reps = &rule.r_vowel;
554
+ } else {
555
+ reps = &rule.r_other;
556
+ }
557
+
558
+ // ``mn`` / ``nm`` always emit both digits, even when adjacent
559
+ // to a duplicate emission (Apache Commons Codec rule).
560
+ const char this_char = cleaned[pos];
561
+ const bool force = (last_char == 'm' && this_char == 'n') ||
562
+ (last_char == 'n' && this_char == 'm');
563
+
564
+ const bool branching_required = branching && reps->size() > 1;
565
+ std::vector<Branch> next;
566
+ if (branching) next.reserve(current.size() * reps->size());
567
+ for (auto& b : current) {
568
+ for (const auto& rep : *reps) {
569
+ if (branching_required) {
570
+ Branch nb = b;
571
+ process_replacement(nb, rep, force);
572
+ next.push_back(std::move(nb));
573
+ } else {
574
+ process_replacement(b, rep, force);
575
+ if (!branching) break;
576
+ next.push_back(b);
577
+ }
578
+ }
579
+ }
580
+ if (branching) {
581
+ // Dedupe by current text (LinkedHashSet semantics in Java).
582
+ std::vector<Branch> uniq;
583
+ uniq.reserve(next.size());
584
+ for (auto& nb : next) {
585
+ bool found = false;
586
+ for (const auto& u : uniq) if (u.text == nb.text) { found = true; break; }
587
+ if (!found) uniq.push_back(std::move(nb));
588
+ }
589
+ current.swap(uniq);
590
+ }
591
+ pos += rule.pattern.size();
592
+ last_char = this_char;
593
+ matched = true;
594
+ break;
595
+ }
596
+ if (!matched) {
597
+ ++pos;
598
+ }
599
+ }
600
+
601
+ // Pad each branch with '0' to length kMaxLength and join with '|'.
602
+ std::string out;
603
+ for (std::size_t k = 0; k < current.size(); ++k) {
604
+ while (current[k].text.size() < kMaxLength) current[k].text.push_back('0');
605
+ if (k != 0) out.push_back('|');
606
+ out.append(current[k].text);
607
+ }
608
+ return out;
609
+ }
610
+
611
+ } // namespace daitch_mokotoff_detail
612
+
613
+ // Encode ``input`` as a Daitch-Mokotoff Soundex set. The engine runs
614
+ // in codepoint space — Python ``str`` storage is fixed-width per
615
+ // string (``PyUnicode_KIND`` is 1/2/4 bytes per codepoint), so the
616
+ // dispatch wrapper widens that storage straight into
617
+ // ``std::vector<std::uint32_t>`` without UTF-8 round-tripping on the
618
+ // input side. The output is a UTF-8 byte string of 6-digit codes.
619
+ //
620
+ // When ``branching`` is true (default) the result is a ``|``-
621
+ // separated list of distinct codes; otherwise a single code.
622
+ // ``folding`` (default true) applies the ASCII fold table for
623
+ // accented characters before encoding.
624
+ inline std::string daitch_mokotoff(const std::vector<std::uint32_t>& input,
625
+ bool branching = true,
626
+ bool folding = true) {
627
+ return daitch_mokotoff_detail::daitch_mokotoff_impl(
628
+ input, branching, folding);
629
+ }
630
+
631
+ } // namespace stride_align::phonetic