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,108 @@
1
+ #pragma once
2
+
3
+ // American Soundex (Russell & Odell, 1918; codified by the US Census).
4
+ //
5
+ // Standard rules:
6
+ // 1. First letter is kept verbatim (upper-cased) as the leading
7
+ // character of the four-character output.
8
+ // 2. Remaining letters are mapped to digit codes:
9
+ // A E I O U Y -> 0 (vowels — break adjacency, don't emit)
10
+ // H W -> transparent (don't emit, don't break
11
+ // adjacency; H/W between two same-coded
12
+ // letters lets them still collapse)
13
+ // B F P V -> 1
14
+ // C G J K Q S X Z -> 2
15
+ // D T -> 3
16
+ // L -> 4
17
+ // M N -> 5
18
+ // R -> 6
19
+ // 3. Adjacent letters mapping to the same digit are collapsed.
20
+ // 4. Vowels (including Y) break adjacency. H and W do not.
21
+ // 5. Output is padded with '0' to length 4, or truncated to 4.
22
+ //
23
+ // Non-ASCII codepoints in str inputs are skipped — Soundex is an
24
+ // ASCII algorithm by construction. Callers wanting accent-folding
25
+ // pre-normalise with ``unicodedata.normalize('NFKD',
26
+ // s).encode('ascii', 'ignore').decode()`` (matches jellyfish's
27
+ // behaviour) before calling.
28
+
29
+ #include <cstddef>
30
+ #include <cstdint>
31
+ #include <string>
32
+ #include <string_view>
33
+
34
+ namespace stride_align::phonetic {
35
+
36
+ namespace soundex_detail {
37
+
38
+ // Codes for A..Z; '0' for vowels (AEIOUY) and H/W. The collapse
39
+ // rule treats H/W specially (transparent); this table just gives
40
+ // the digit value, the loop handles the transparency.
41
+ // A B C D E F
42
+ constexpr char kSoundexCode[26] = { '0', '1', '2', '3', '0', '1',
43
+ // G H I J K L
44
+ '2', '0', '0', '2', '2', '4',
45
+ // M N O P Q R
46
+ '5', '5', '0', '1', '2', '6',
47
+ // S T U V W X
48
+ '2', '3', '0', '1', '0', '2',
49
+ // Y Z
50
+ '0', '2'};
51
+
52
+ inline constexpr char to_upper_ascii(char c) noexcept {
53
+ return (c >= 'a' && c <= 'z') ? static_cast<char>(c - ('a' - 'A')) : c;
54
+ }
55
+
56
+ inline constexpr bool is_upper_alpha(char c) noexcept {
57
+ return c >= 'A' && c <= 'Z';
58
+ }
59
+
60
+ } // namespace soundex_detail
61
+
62
+ // Compute the Soundex encoding of ``input`` (interpreted as a
63
+ // sequence of ASCII bytes — non-letters and non-ASCII bytes are
64
+ // skipped). Returns a 4-character string when ``input`` contains
65
+ // at least one ASCII letter, else an empty string.
66
+ inline std::string soundex(std::string_view input) {
67
+ using namespace soundex_detail;
68
+
69
+ std::string out;
70
+
71
+ // Find first ASCII letter.
72
+ std::size_t i = 0;
73
+ for (; i < input.size(); ++i) {
74
+ if (is_upper_alpha(to_upper_ascii(input[i]))) break;
75
+ }
76
+ if (i >= input.size()) return out;
77
+
78
+ out.reserve(4);
79
+ const char first = to_upper_ascii(input[i]);
80
+ out.push_back(first);
81
+ char last_code = kSoundexCode[first - 'A'];
82
+
83
+ for (++i; i < input.size() && out.size() < 4; ++i) {
84
+ const char c = to_upper_ascii(input[i]);
85
+ if (!is_upper_alpha(c)) continue;
86
+
87
+ // H and W are transparent: don't emit, don't break adjacency.
88
+ if (c == 'H' || c == 'W') continue;
89
+
90
+ const char code = kSoundexCode[c - 'A'];
91
+
92
+ if (code == '0') {
93
+ // True vowel (AEIOUY): breaks adjacency for the collapse
94
+ // rule but emits nothing.
95
+ last_code = '0';
96
+ continue;
97
+ }
98
+
99
+ if (code != last_code) out.push_back(code);
100
+ last_code = code;
101
+ }
102
+
103
+ // Pad to length 4.
104
+ while (out.size() < 4) out.push_back('0');
105
+ return out;
106
+ }
107
+
108
+ } // namespace stride_align::phonetic
@@ -0,0 +1,445 @@
1
+ #pragma once
2
+
3
+ // ``sa.token_sort_ratio`` and ``sa.token_set_ratio`` kernels.
4
+ //
5
+ // Both functions used to live in ``src/stride_align/_fuzz.py`` as pure
6
+ // Python: split-on-whitespace, set algebra, sort, join, then call into
7
+ // the Indel kernel. Profiling on a representative ``m=54 n=66`` pair
8
+ // showed 71% of token_set_ratio's per-call time was Python overhead
9
+ // (tokenisation + set ops + sort + join), only 18% was the actual
10
+ // Indel kernel. Lifting the whole recipe into C++ closes that gap.
11
+ //
12
+ // Tokenisation is whitespace-only (``' '``, ``'\t'``, ``'\n'``,
13
+ // ``'\r'``) to match the Python ``str.split()`` convention. Tokens
14
+ // are returned as views over the input buffer — no copies — so the
15
+ // per-call allocation is one ``std::vector<std::span<...>>`` for the
16
+ // token list plus one output buffer for each candidate string built
17
+ // by the set-ratio path.
18
+ //
19
+ // Algorithm (matches the Python reference bit-exactly):
20
+ //
21
+ // token_sort_ratio(a, b):
22
+ // ja = sort_join(tokens(a))
23
+ // jb = sort_join(tokens(b))
24
+ // return indel_normalized(ja, jb)
25
+ //
26
+ // token_set_ratio(a, b):
27
+ // ta = dedupe_sorted(tokens(a))
28
+ // tb = dedupe_sorted(tokens(b))
29
+ // intersect, diff_a, diff_b = three-way merge over (ta, tb)
30
+ // t0 = join(intersect)
31
+ // t1 = join(intersect ++ diff_a) # concat, NOT re-merged
32
+ // t2 = join(intersect ++ diff_b) # "
33
+ // return max(indel_normalized(t0, t1),
34
+ // indel_normalized(t0, t2),
35
+ // indel_normalized(t1, t2))
36
+ //
37
+ // The "concat NOT re-merged" detail matters: rapidfuzz preserves
38
+ // ``t1 = intersect_sorted then diff_a_sorted``, which is NOT the
39
+ // same as ``sorted(intersect ∪ diff_a)`` whenever the two have
40
+ // interleaving lex order. Tests pin this against rapidfuzz.
41
+
42
+ #include <algorithm>
43
+ #include <cstddef>
44
+ #include <cstdint>
45
+ #include <span>
46
+ #include <type_traits>
47
+ #include <vector>
48
+
49
+ #include "stride_align/indel.hpp"
50
+ #include "stride_align/lcs.hpp"
51
+
52
+ namespace stride_align::token_ratios {
53
+
54
+ using Codepoint = ::stride_align::lcs::Codepoint;
55
+
56
+ // Whitespace classifier. Matches Python's ``str.split()`` default
57
+ // (split on any sequence of ASCII whitespace).
58
+ template <typename Token>
59
+ inline bool is_ws(Token c) noexcept {
60
+ return c == Token{' '} || c == Token{'\t'} ||
61
+ c == Token{'\n'} || c == Token{'\r'};
62
+ }
63
+
64
+ // Split ``s`` on runs of whitespace, returning views over the input
65
+ // buffer (no copies). Empty input yields an empty vector. Leading and
66
+ // trailing whitespace is skipped (matches ``str.split()``).
67
+ template <typename Token>
68
+ inline std::vector<std::span<const Token>> tokens_of(
69
+ std::span<const Token> s) {
70
+ std::vector<std::span<const Token>> out;
71
+ std::size_t i = 0;
72
+ while (i < s.size()) {
73
+ while (i < s.size() && is_ws(s[i])) ++i;
74
+ if (i >= s.size()) break;
75
+ const std::size_t start = i;
76
+ while (i < s.size() && !is_ws(s[i])) ++i;
77
+ out.emplace_back(s.data() + start, i - start);
78
+ }
79
+ return out;
80
+ }
81
+
82
+ // Lexicographic comparison of two token spans. Used for sorting and
83
+ // the three-way set-merge.
84
+ template <typename Token>
85
+ struct LexLess {
86
+ bool operator()(std::span<const Token> a,
87
+ std::span<const Token> b) const noexcept {
88
+ const std::size_t n = std::min(a.size(), b.size());
89
+ for (std::size_t i = 0; i < n; ++i) {
90
+ if (a[i] != b[i]) return a[i] < b[i];
91
+ }
92
+ return a.size() < b.size();
93
+ }
94
+ };
95
+
96
+ template <typename Token>
97
+ struct LexEqual {
98
+ bool operator()(std::span<const Token> a,
99
+ std::span<const Token> b) const noexcept {
100
+ return std::equal(a.begin(), a.end(), b.begin(), b.end());
101
+ }
102
+ };
103
+
104
+ // Join token spans with a single space separator, writing into
105
+ // ``out``. Caller may reuse ``out`` across calls — it's cleared before
106
+ // writing.
107
+ template <typename Token>
108
+ inline void join_with_space(
109
+ const std::vector<std::span<const Token>>& parts,
110
+ std::vector<Token>& out) {
111
+ std::size_t total = 0;
112
+ if (!parts.empty()) {
113
+ for (const auto& p : parts) total += p.size();
114
+ total += parts.size() - 1U; // separators
115
+ }
116
+ out.clear();
117
+ out.reserve(total);
118
+ for (std::size_t k = 0; k < parts.size(); ++k) {
119
+ if (k > 0) out.push_back(Token{' '});
120
+ out.insert(out.end(), parts[k].begin(), parts[k].end());
121
+ }
122
+ }
123
+
124
+ // Indel-normalised similarity for two prepared sequences:
125
+ //
126
+ // 1 - indel(a, b) / (|a| + |b|)
127
+ //
128
+ // Empty-both convention matches rapidfuzz: ``("", "") -> 1.0``.
129
+ template <typename Token>
130
+ inline double indel_normalized(
131
+ std::span<const Token> a,
132
+ std::span<const Token> b) {
133
+ if (a.empty() && b.empty()) return 1.0;
134
+ const std::size_t total = a.size() + b.size();
135
+ if (total == 0U) return 1.0;
136
+ std::size_t d;
137
+ if constexpr (std::is_same_v<Token, std::uint8_t>) {
138
+ // Flat 256-entry PEQ + direct indexing — the bit-parallel byte path
139
+ // fuzz.ratio already uses. The generic indel_distance<Token> builds an
140
+ // unordered_map PEQ and hash-looks-up per text char; that kernel (not
141
+ // the tokenise/sort/join) was the token-ratio gap vs rapidfuzz.
142
+ d = ::stride_align::indel::indel_distance_u8(a, b);
143
+ } else {
144
+ d = ::stride_align::indel::indel_distance<Token>(a, b);
145
+ }
146
+ return 1.0 - static_cast<double>(d) / static_cast<double>(total);
147
+ }
148
+
149
+ // Forward declarations of the thread-local scratch type and accessor
150
+ // used by the engines below. The full definitions appear later in
151
+ // the file — they need the engines' helper types.
152
+ template <typename Token>
153
+ struct TokenWorkspace;
154
+ template <typename Token>
155
+ inline TokenWorkspace<Token>& token_workspace();
156
+
157
+ // token_sort_ratio engine. Uses thread-local scratch buffers for
158
+ // the token lists and the joined output, so consecutive calls reuse
159
+ // allocations.
160
+ template <typename Token>
161
+ inline double token_sort_ratio_engine(
162
+ std::span<const Token> a,
163
+ std::span<const Token> b);
164
+
165
+ // token_set_ratio engine. Same thread-local scratch as the sort
166
+ // engine — the two share buffers when called in sequence (e.g. by
167
+ // the WRatio path below).
168
+ template <typename Token>
169
+ inline double token_set_ratio_engine(
170
+ std::span<const Token> a,
171
+ std::span<const Token> b);
172
+
173
+ // Public byte-fast-path-or-codepoint dispatchers. The byte fast path
174
+ // drops to ``std::uint8_t`` token type when every codepoint in both
175
+ // inputs is < 256, which routes the per-window Indel call through the
176
+ // flat 256-row PEQ and the K-specialised multi-word kernel.
177
+ inline double token_sort_ratio(
178
+ const std::vector<Codepoint>& a,
179
+ const std::vector<Codepoint>& b) {
180
+ bool fits_in_byte = true;
181
+ for (const auto cp : a) { if (cp >= 256U) { fits_in_byte = false; break; } }
182
+ if (fits_in_byte) {
183
+ for (const auto cp : b) { if (cp >= 256U) { fits_in_byte = false; break; } }
184
+ }
185
+ if (fits_in_byte) {
186
+ std::vector<std::uint8_t> ab(a.begin(), a.end());
187
+ std::vector<std::uint8_t> bb(b.begin(), b.end());
188
+ return token_sort_ratio_engine<std::uint8_t>(
189
+ std::span<const std::uint8_t>(ab),
190
+ std::span<const std::uint8_t>(bb));
191
+ }
192
+ return token_sort_ratio_engine<Codepoint>(
193
+ std::span<const Codepoint>(a),
194
+ std::span<const Codepoint>(b));
195
+ }
196
+
197
+ // In-place tokenisation variant that writes into a caller-owned
198
+ // buffer. Used by the combined WRatio token path that shares a
199
+ // thread-local scratch across token_sort + token_set computations.
200
+ template <typename Token>
201
+ inline void tokens_of_inplace(
202
+ std::span<const Token> s,
203
+ std::vector<std::span<const Token>>& out) {
204
+ out.clear();
205
+ std::size_t i = 0;
206
+ while (i < s.size()) {
207
+ while (i < s.size() && is_ws(s[i])) ++i;
208
+ if (i >= s.size()) break;
209
+ const std::size_t start = i;
210
+ while (i < s.size() && !is_ws(s[i])) ++i;
211
+ out.emplace_back(s.data() + start, i - start);
212
+ }
213
+ }
214
+
215
+ // Thread-local scratch for the combined token-sort + token-set path.
216
+ // Carries every per-call buffer (token lists, sorted-vs-unique
217
+ // views, candidate joins) so consecutive WRatio calls reuse the
218
+ // allocations.
219
+ template <typename Token>
220
+ struct TokenWorkspace {
221
+ std::vector<std::span<const Token>> tokens_a, tokens_b;
222
+ std::vector<std::span<const Token>> intersect, diff_a, diff_b;
223
+ std::vector<std::span<const Token>> t1_parts, t2_parts;
224
+ std::vector<Token> ja, jb; // sort_ratio joined strings
225
+ std::vector<Token> t0, t1, t2; // set_ratio candidate strings
226
+ };
227
+
228
+ template <typename Token>
229
+ inline TokenWorkspace<Token>& token_workspace() {
230
+ thread_local TokenWorkspace<Token> w;
231
+ return w;
232
+ }
233
+
234
+ // Combined entry: compute both token_sort_ratio AND token_set_ratio
235
+ // in one pass over the inputs. Used by ``wratio_engine`` in the
236
+ // length-similar branch (``len_ratio < 1.5``), where both ratios are
237
+ // needed. Tokenises ``a`` and ``b`` ONCE, sorts ONCE (with
238
+ // duplicates), reuses the sorted views for the dedupe + set-merge,
239
+ // and shares the thread-local join buffers across the 1 + 3 indel
240
+ // calls. Writes the two ratios into the out-params.
241
+ template <typename Token>
242
+ inline void compute_token_sort_and_set_ratio(
243
+ std::span<const Token> a,
244
+ std::span<const Token> b,
245
+ double& sort_score,
246
+ double& set_score) {
247
+ auto& w = token_workspace<Token>();
248
+ tokens_of_inplace(a, w.tokens_a);
249
+ tokens_of_inplace(b, w.tokens_b);
250
+
251
+ // Edge cases mirror the standalone engines.
252
+ const bool a_empty = w.tokens_a.empty();
253
+ const bool b_empty = w.tokens_b.empty();
254
+ if (a_empty && b_empty) {
255
+ sort_score = 1.0;
256
+ set_score = 0.0; // rapidfuzz convention: empty/empty token_set is 0
257
+ return;
258
+ }
259
+ if (a_empty || b_empty) {
260
+ sort_score = 0.0;
261
+ set_score = 0.0;
262
+ return;
263
+ }
264
+
265
+ const LexLess<Token> lt;
266
+ std::sort(w.tokens_a.begin(), w.tokens_a.end(), lt);
267
+ std::sort(w.tokens_b.begin(), w.tokens_b.end(), lt);
268
+
269
+ // --- token_sort_ratio leg: join the (sorted, with duplicates) views.
270
+ join_with_space(w.tokens_a, w.ja);
271
+ join_with_space(w.tokens_b, w.jb);
272
+ sort_score = indel_normalized<Token>(
273
+ std::span<const Token>(w.ja.data(), w.ja.size()),
274
+ std::span<const Token>(w.jb.data(), w.jb.size()));
275
+
276
+ // --- token_set_ratio leg: dedupe the already-sorted token lists
277
+ // in place. The set algebra then runs on the unique views without
278
+ // a second sort pass.
279
+ w.tokens_a.erase(std::unique(w.tokens_a.begin(), w.tokens_a.end(),
280
+ LexEqual<Token>{}),
281
+ w.tokens_a.end());
282
+ w.tokens_b.erase(std::unique(w.tokens_b.begin(), w.tokens_b.end(),
283
+ LexEqual<Token>{}),
284
+ w.tokens_b.end());
285
+
286
+ w.intersect.clear();
287
+ w.diff_a.clear();
288
+ w.diff_b.clear();
289
+ std::size_t i = 0, j = 0;
290
+ while (i < w.tokens_a.size() && j < w.tokens_b.size()) {
291
+ if (lt(w.tokens_a[i], w.tokens_b[j])) {
292
+ w.diff_a.push_back(w.tokens_a[i]); ++i;
293
+ } else if (lt(w.tokens_b[j], w.tokens_a[i])) {
294
+ w.diff_b.push_back(w.tokens_b[j]); ++j;
295
+ } else {
296
+ w.intersect.push_back(w.tokens_a[i]); ++i; ++j;
297
+ }
298
+ }
299
+ while (i < w.tokens_a.size()) w.diff_a.push_back(w.tokens_a[i++]);
300
+ while (j < w.tokens_b.size()) w.diff_b.push_back(w.tokens_b[j++]);
301
+
302
+ if (w.diff_a.empty() && w.diff_b.empty()) {
303
+ set_score = 1.0;
304
+ return;
305
+ }
306
+
307
+ w.t1_parts.assign(w.intersect.begin(), w.intersect.end());
308
+ w.t1_parts.insert(w.t1_parts.end(), w.diff_a.begin(), w.diff_a.end());
309
+ w.t2_parts.assign(w.intersect.begin(), w.intersect.end());
310
+ w.t2_parts.insert(w.t2_parts.end(), w.diff_b.begin(), w.diff_b.end());
311
+
312
+ join_with_space(w.intersect, w.t0);
313
+ join_with_space(w.t1_parts, w.t1);
314
+ join_with_space(w.t2_parts, w.t2);
315
+
316
+ const double r0 = indel_normalized<Token>(
317
+ std::span<const Token>(w.t0.data(), w.t0.size()),
318
+ std::span<const Token>(w.t1.data(), w.t1.size()));
319
+ if (r0 >= 1.0) { set_score = 1.0; return; }
320
+ const double r1 = indel_normalized<Token>(
321
+ std::span<const Token>(w.t0.data(), w.t0.size()),
322
+ std::span<const Token>(w.t2.data(), w.t2.size()));
323
+ if (r1 >= 1.0) { set_score = 1.0; return; }
324
+ const double r2 = indel_normalized<Token>(
325
+ std::span<const Token>(w.t1.data(), w.t1.size()),
326
+ std::span<const Token>(w.t2.data(), w.t2.size()));
327
+ set_score = std::max({r0, r1, r2});
328
+ }
329
+
330
+ // Standalone engine definitions (forward-declared earlier in the file
331
+ // because they need ``TokenWorkspace``).
332
+
333
+ template <typename Token>
334
+ inline double token_sort_ratio_engine(
335
+ std::span<const Token> a,
336
+ std::span<const Token> b) {
337
+ auto& w = token_workspace<Token>();
338
+ tokens_of_inplace(a, w.tokens_a);
339
+ tokens_of_inplace(b, w.tokens_b);
340
+ if (w.tokens_a.empty() && w.tokens_b.empty()) {
341
+ return 1.0;
342
+ }
343
+ std::sort(w.tokens_a.begin(), w.tokens_a.end(), LexLess<Token>{});
344
+ std::sort(w.tokens_b.begin(), w.tokens_b.end(), LexLess<Token>{});
345
+ join_with_space(w.tokens_a, w.ja);
346
+ join_with_space(w.tokens_b, w.jb);
347
+ return indel_normalized<Token>(
348
+ std::span<const Token>(w.ja.data(), w.ja.size()),
349
+ std::span<const Token>(w.jb.data(), w.jb.size()));
350
+ }
351
+
352
+ template <typename Token>
353
+ inline double token_set_ratio_engine(
354
+ std::span<const Token> a,
355
+ std::span<const Token> b) {
356
+ auto& w = token_workspace<Token>();
357
+ tokens_of_inplace(a, w.tokens_a);
358
+ tokens_of_inplace(b, w.tokens_b);
359
+ if (w.tokens_a.empty() || w.tokens_b.empty()) return 0.0;
360
+
361
+ const LexLess<Token> lt;
362
+ std::sort(w.tokens_a.begin(), w.tokens_a.end(), lt);
363
+ w.tokens_a.erase(std::unique(w.tokens_a.begin(), w.tokens_a.end(),
364
+ LexEqual<Token>{}),
365
+ w.tokens_a.end());
366
+ std::sort(w.tokens_b.begin(), w.tokens_b.end(), lt);
367
+ w.tokens_b.erase(std::unique(w.tokens_b.begin(), w.tokens_b.end(),
368
+ LexEqual<Token>{}),
369
+ w.tokens_b.end());
370
+
371
+ w.intersect.clear();
372
+ w.diff_a.clear();
373
+ w.diff_b.clear();
374
+ std::size_t i = 0, j = 0;
375
+ while (i < w.tokens_a.size() && j < w.tokens_b.size()) {
376
+ if (lt(w.tokens_a[i], w.tokens_b[j])) {
377
+ w.diff_a.push_back(w.tokens_a[i]); ++i;
378
+ } else if (lt(w.tokens_b[j], w.tokens_a[i])) {
379
+ w.diff_b.push_back(w.tokens_b[j]); ++j;
380
+ } else {
381
+ w.intersect.push_back(w.tokens_a[i]); ++i; ++j;
382
+ }
383
+ }
384
+ while (i < w.tokens_a.size()) w.diff_a.push_back(w.tokens_a[i++]);
385
+ while (j < w.tokens_b.size()) w.diff_b.push_back(w.tokens_b[j++]);
386
+
387
+ if (w.diff_a.empty() && w.diff_b.empty()) return 1.0;
388
+
389
+ w.t1_parts.assign(w.intersect.begin(), w.intersect.end());
390
+ w.t1_parts.insert(w.t1_parts.end(), w.diff_a.begin(), w.diff_a.end());
391
+ w.t2_parts.assign(w.intersect.begin(), w.intersect.end());
392
+ w.t2_parts.insert(w.t2_parts.end(), w.diff_b.begin(), w.diff_b.end());
393
+
394
+ join_with_space(w.intersect, w.t0);
395
+ join_with_space(w.t1_parts, w.t1);
396
+ join_with_space(w.t2_parts, w.t2);
397
+
398
+ const double r0 = indel_normalized<Token>(
399
+ std::span<const Token>(w.t0.data(), w.t0.size()),
400
+ std::span<const Token>(w.t1.data(), w.t1.size()));
401
+ if (r0 >= 1.0) return 1.0;
402
+ const double r1 = indel_normalized<Token>(
403
+ std::span<const Token>(w.t0.data(), w.t0.size()),
404
+ std::span<const Token>(w.t2.data(), w.t2.size()));
405
+ if (r1 >= 1.0) return 1.0;
406
+ const double r2 = indel_normalized<Token>(
407
+ std::span<const Token>(w.t1.data(), w.t1.size()),
408
+ std::span<const Token>(w.t2.data(), w.t2.size()));
409
+ return std::max({r0, r1, r2});
410
+ }
411
+
412
+ // Byte fast path entries: caller has already established
413
+ // byte-compatible inputs (ASCII Python str or bytes-like).
414
+ inline double token_sort_ratio_bytes(
415
+ std::span<const std::uint8_t> a,
416
+ std::span<const std::uint8_t> b) {
417
+ return token_sort_ratio_engine<std::uint8_t>(a, b);
418
+ }
419
+ inline double token_set_ratio_bytes(
420
+ std::span<const std::uint8_t> a,
421
+ std::span<const std::uint8_t> b) {
422
+ return token_set_ratio_engine<std::uint8_t>(a, b);
423
+ }
424
+
425
+ inline double token_set_ratio(
426
+ const std::vector<Codepoint>& a,
427
+ const std::vector<Codepoint>& b) {
428
+ bool fits_in_byte = true;
429
+ for (const auto cp : a) { if (cp >= 256U) { fits_in_byte = false; break; } }
430
+ if (fits_in_byte) {
431
+ for (const auto cp : b) { if (cp >= 256U) { fits_in_byte = false; break; } }
432
+ }
433
+ if (fits_in_byte) {
434
+ std::vector<std::uint8_t> ab(a.begin(), a.end());
435
+ std::vector<std::uint8_t> bb(b.begin(), b.end());
436
+ return token_set_ratio_engine<std::uint8_t>(
437
+ std::span<const std::uint8_t>(ab),
438
+ std::span<const std::uint8_t>(bb));
439
+ }
440
+ return token_set_ratio_engine<Codepoint>(
441
+ std::span<const Codepoint>(a),
442
+ std::span<const Codepoint>(b));
443
+ }
444
+
445
+ } // namespace stride_align::token_ratios
@@ -0,0 +1,16 @@
1
+ #pragma once
2
+
3
+ #include <cstdint>
4
+
5
+ namespace stride_align {
6
+
7
+ using Score = std::int64_t;
8
+
9
+ enum class KernelBits : std::uint8_t {
10
+ bits8 = 8,
11
+ bits16 = 16,
12
+ bits32 = 32,
13
+ bits64 = 64,
14
+ };
15
+
16
+ } // namespace stride_align