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.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/NOTICE +255 -0
- data/README.md +107 -0
- data/data/bmpm_data/gen_approx_any.txt +131 -0
- data/data/bmpm_data/gen_approx_arabic.txt +26 -0
- data/data/bmpm_data/gen_approx_common.txt +233 -0
- data/data/bmpm_data/gen_approx_cyrillic.txt +18 -0
- data/data/bmpm_data/gen_approx_czech.txt +18 -0
- data/data/bmpm_data/gen_approx_dutch.txt +18 -0
- data/data/bmpm_data/gen_approx_english.txt +47 -0
- data/data/bmpm_data/gen_approx_french.txt +25 -0
- data/data/bmpm_data/gen_approx_german.txt +73 -0
- data/data/bmpm_data/gen_approx_greek.txt +18 -0
- data/data/bmpm_data/gen_approx_greeklatin.txt +20 -0
- data/data/bmpm_data/gen_approx_hebrew.txt +18 -0
- data/data/bmpm_data/gen_approx_hungarian.txt +18 -0
- data/data/bmpm_data/gen_approx_italian.txt +18 -0
- data/data/bmpm_data/gen_approx_polish.txt +84 -0
- data/data/bmpm_data/gen_approx_portuguese.txt +18 -0
- data/data/bmpm_data/gen_approx_romanian.txt +18 -0
- data/data/bmpm_data/gen_approx_russian.txt +48 -0
- data/data/bmpm_data/gen_approx_spanish.txt +21 -0
- data/data/bmpm_data/gen_approx_turkish.txt +18 -0
- data/data/bmpm_data/gen_exact_any.txt +40 -0
- data/data/bmpm_data/gen_exact_approx_common.txt +79 -0
- data/data/bmpm_data/gen_exact_arabic.txt +18 -0
- data/data/bmpm_data/gen_exact_common.txt +32 -0
- data/data/bmpm_data/gen_exact_cyrillic.txt +18 -0
- data/data/bmpm_data/gen_exact_czech.txt +18 -0
- data/data/bmpm_data/gen_exact_dutch.txt +18 -0
- data/data/bmpm_data/gen_exact_english.txt +18 -0
- data/data/bmpm_data/gen_exact_french.txt +18 -0
- data/data/bmpm_data/gen_exact_german.txt +18 -0
- data/data/bmpm_data/gen_exact_greek.txt +18 -0
- data/data/bmpm_data/gen_exact_greeklatin.txt +18 -0
- data/data/bmpm_data/gen_exact_hebrew.txt +18 -0
- data/data/bmpm_data/gen_exact_hungarian.txt +18 -0
- data/data/bmpm_data/gen_exact_italian.txt +18 -0
- data/data/bmpm_data/gen_exact_polish.txt +23 -0
- data/data/bmpm_data/gen_exact_portuguese.txt +18 -0
- data/data/bmpm_data/gen_exact_romanian.txt +18 -0
- data/data/bmpm_data/gen_exact_russian.txt +19 -0
- data/data/bmpm_data/gen_exact_spanish.txt +19 -0
- data/data/bmpm_data/gen_exact_turkish.txt +18 -0
- data/data/bmpm_data/gen_hebrew_common.txt +113 -0
- data/data/bmpm_data/gen_lang.txt +295 -0
- data/data/bmpm_data/gen_languages.txt +36 -0
- data/data/bmpm_data/gen_rules_any.txt +367 -0
- data/data/bmpm_data/gen_rules_arabic.txt +76 -0
- data/data/bmpm_data/gen_rules_cyrillic.txt +99 -0
- data/data/bmpm_data/gen_rules_czech.txt +67 -0
- data/data/bmpm_data/gen_rules_dutch.txt +78 -0
- data/data/bmpm_data/gen_rules_english.txt +113 -0
- data/data/bmpm_data/gen_rules_french.txt +114 -0
- data/data/bmpm_data/gen_rules_german.txt +129 -0
- data/data/bmpm_data/gen_rules_greek.txt +97 -0
- data/data/bmpm_data/gen_rules_greeklatin.txt +118 -0
- data/data/bmpm_data/gen_rules_hebrew.txt +62 -0
- data/data/bmpm_data/gen_rules_hungarian.txt +83 -0
- data/data/bmpm_data/gen_rules_italian.txt +77 -0
- data/data/bmpm_data/gen_rules_polish.txt +185 -0
- data/data/bmpm_data/gen_rules_portuguese.txt +105 -0
- data/data/bmpm_data/gen_rules_romanian.txt +64 -0
- data/data/bmpm_data/gen_rules_russian.txt +142 -0
- data/data/bmpm_data/gen_rules_spanish.txt +85 -0
- data/data/bmpm_data/gen_rules_turkish.txt +50 -0
- data/data/keyboard_data/qwerty.npy +0 -0
- data/data/matrix_data/BLOSUM100 +31 -0
- data/data/matrix_data/BLOSUM30 +31 -0
- data/data/matrix_data/BLOSUM35 +31 -0
- data/data/matrix_data/BLOSUM40 +31 -0
- data/data/matrix_data/BLOSUM45 +25 -0
- data/data/matrix_data/BLOSUM50 +25 -0
- data/data/matrix_data/BLOSUM55 +31 -0
- data/data/matrix_data/BLOSUM60 +31 -0
- data/data/matrix_data/BLOSUM62 +25 -0
- data/data/matrix_data/BLOSUM65 +31 -0
- data/data/matrix_data/BLOSUM70 +31 -0
- data/data/matrix_data/BLOSUM75 +31 -0
- data/data/matrix_data/BLOSUM80 +25 -0
- data/data/matrix_data/BLOSUM85 +31 -0
- data/data/matrix_data/BLOSUM90 +25 -0
- data/data/matrix_data/NUC.4.4 +25 -0
- data/data/matrix_data/PAM10 +34 -0
- data/data/matrix_data/PAM100 +34 -0
- data/data/matrix_data/PAM110 +34 -0
- data/data/matrix_data/PAM120 +34 -0
- data/data/matrix_data/PAM130 +34 -0
- data/data/matrix_data/PAM140 +34 -0
- data/data/matrix_data/PAM150 +34 -0
- data/data/matrix_data/PAM160 +34 -0
- data/data/matrix_data/PAM170 +34 -0
- data/data/matrix_data/PAM180 +34 -0
- data/data/matrix_data/PAM190 +34 -0
- data/data/matrix_data/PAM20 +34 -0
- data/data/matrix_data/PAM200 +34 -0
- data/data/matrix_data/PAM210 +34 -0
- data/data/matrix_data/PAM220 +34 -0
- data/data/matrix_data/PAM230 +34 -0
- data/data/matrix_data/PAM240 +34 -0
- data/data/matrix_data/PAM250 +25 -0
- data/data/matrix_data/PAM260 +34 -0
- data/data/matrix_data/PAM270 +34 -0
- data/data/matrix_data/PAM280 +34 -0
- data/data/matrix_data/PAM290 +34 -0
- data/data/matrix_data/PAM30 +25 -0
- data/data/matrix_data/PAM300 +34 -0
- data/data/matrix_data/PAM310 +34 -0
- data/data/matrix_data/PAM320 +34 -0
- data/data/matrix_data/PAM330 +34 -0
- data/data/matrix_data/PAM340 +34 -0
- data/data/matrix_data/PAM350 +34 -0
- data/data/matrix_data/PAM360 +34 -0
- data/data/matrix_data/PAM370 +34 -0
- data/data/matrix_data/PAM380 +34 -0
- data/data/matrix_data/PAM390 +34 -0
- data/data/matrix_data/PAM40 +34 -0
- data/data/matrix_data/PAM400 +34 -0
- data/data/matrix_data/PAM410 +34 -0
- data/data/matrix_data/PAM420 +34 -0
- data/data/matrix_data/PAM430 +34 -0
- data/data/matrix_data/PAM440 +34 -0
- data/data/matrix_data/PAM450 +34 -0
- data/data/matrix_data/PAM460 +34 -0
- data/data/matrix_data/PAM470 +34 -0
- data/data/matrix_data/PAM480 +34 -0
- data/data/matrix_data/PAM490 +34 -0
- data/data/matrix_data/PAM50 +34 -0
- data/data/matrix_data/PAM500 +34 -0
- data/data/matrix_data/PAM60 +34 -0
- data/data/matrix_data/PAM70 +25 -0
- data/data/matrix_data/PAM80 +34 -0
- data/data/matrix_data/PAM90 +34 -0
- data/ext/stride_align/backend_avx2.cpp +2 -0
- data/ext/stride_align/backend_avx512bwvl.cpp +2 -0
- data/ext/stride_align/backend_generic.cpp +3 -0
- data/ext/stride_align/backend_impl.hpp +983 -0
- data/ext/stride_align/backend_lasx.cpp +2 -0
- data/ext/stride_align/backend_lsx.cpp +2 -0
- data/ext/stride_align/backend_neon.cpp +2 -0
- data/ext/stride_align/backend_rvv.cpp +2 -0
- data/ext/stride_align/backend_sse41.cpp +2 -0
- data/ext/stride_align/backend_sve.cpp +2 -0
- data/ext/stride_align/backend_sve2.cpp +2 -0
- data/ext/stride_align/backend_vsx.cpp +2 -0
- data/ext/stride_align/beider_morse_impl.cpp +5 -0
- data/ext/stride_align/cpu_detect.cpp +172 -0
- data/ext/stride_align/cpu_detect.hpp +6 -0
- data/ext/stride_align/extconf.rb +114 -0
- data/ext/stride_align/target_profile.hpp +82 -0
- data/ext/stride_align/vendor/beider_morse_impl.cpp +1467 -0
- data/ext/stride_align/vendor/stride_align/alignment.hpp +199 -0
- data/ext/stride_align/vendor/stride_align/batch.hpp +812 -0
- data/ext/stride_align/vendor/stride_align/beider_morse.hpp +121 -0
- data/ext/stride_align/vendor/stride_align/caverphone.hpp +222 -0
- data/ext/stride_align/vendor/stride_align/cologne_phonetic.hpp +202 -0
- data/ext/stride_align/vendor/stride_align/core.hpp +731 -0
- data/ext/stride_align/vendor/stride_align/daitch_mokotoff.hpp +631 -0
- data/ext/stride_align/vendor/stride_align/double_metaphone.hpp +796 -0
- data/ext/stride_align/vendor/stride_align/dtw.hpp +300 -0
- data/ext/stride_align/vendor/stride_align/encoded.hpp +235 -0
- data/ext/stride_align/vendor/stride_align/hamming.hpp +55 -0
- data/ext/stride_align/vendor/stride_align/indel.hpp +1200 -0
- data/ext/stride_align/vendor/stride_align/jaro.hpp +517 -0
- data/ext/stride_align/vendor/stride_align/lcs.hpp +159 -0
- data/ext/stride_align/vendor/stride_align/levenshtein.hpp +1247 -0
- data/ext/stride_align/vendor/stride_align/levenshtein_prepared.hpp +193 -0
- data/ext/stride_align/vendor/stride_align/match_rating.hpp +168 -0
- data/ext/stride_align/vendor/stride_align/metaphone.hpp +291 -0
- data/ext/stride_align/vendor/stride_align/ngram.hpp +176 -0
- data/ext/stride_align/vendor/stride_align/nysiis.hpp +199 -0
- data/ext/stride_align/vendor/stride_align/pairwise_alignment.hpp +465 -0
- data/ext/stride_align/vendor/stride_align/partial_ratio.hpp +486 -0
- data/ext/stride_align/vendor/stride_align/ratcliff_obershelp.hpp +101 -0
- data/ext/stride_align/vendor/stride_align/soundex.hpp +108 -0
- data/ext/stride_align/vendor/stride_align/token_ratios.hpp +445 -0
- data/ext/stride_align/vendor/stride_align/types.hpp +16 -0
- data/ext/stride_align/vendor/stride_align/utf8.hpp +512 -0
- data/ext/stride_align/vendor/stride_align/wratio.hpp +363 -0
- data/lib/stride_align/algorithms.rb +296 -0
- data/lib/stride_align/alignment_path.rb +217 -0
- data/lib/stride_align/backend.rb +47 -0
- data/lib/stride_align/batch.rb +705 -0
- data/lib/stride_align/core.rb +180 -0
- data/lib/stride_align/keyboard.rb +200 -0
- data/lib/stride_align/matrices.rb +403 -0
- data/lib/stride_align/version.rb +5 -0
- data/lib/stride_align.rb +87 -0
- metadata +231 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// Beider-Morse Phonetic Matching (BMPM) — Alexander Beider & Stephen P.
|
|
4
|
+
// Morse, 2008.
|
|
5
|
+
//
|
|
6
|
+
// Transforms a word (typically a family name) into one or more phonetic
|
|
7
|
+
// codes by recognising letter-cluster patterns and emitting alternative
|
|
8
|
+
// pronunciations conditional on plausible source languages. The output
|
|
9
|
+
// is a ``|``-separated string of distinct codes; an exact-match search
|
|
10
|
+
// against the BMPM codes of a candidate list will surface names spelled
|
|
11
|
+
// differently but pronounced similarly.
|
|
12
|
+
//
|
|
13
|
+
// Stride-align ships the **GENERIC** ``NameType`` only — the broad
|
|
14
|
+
// "general-purpose name or word" rule set. The ``ASHKENAZI`` and
|
|
15
|
+
// ``SEPHARDIC`` name types from the upstream Apache Commons Codec
|
|
16
|
+
// distribution are not included.
|
|
17
|
+
//
|
|
18
|
+
// Pipeline (per Beider-Morse, ported from the algorithm description in
|
|
19
|
+
// the upstream Apache Commons Codec 1.18 BMPM classes):
|
|
20
|
+
//
|
|
21
|
+
// 1. The dispatch wrapper hands the engine a ``vector<Codepoint>``
|
|
22
|
+
// widened straight out of ``PyUnicode_DATA`` (zero UTF-8 round-
|
|
23
|
+
// trip on the input side).
|
|
24
|
+
// 2. Lower-case input (ASCII-only fold), replace ``-`` with space,
|
|
25
|
+
// trim.
|
|
26
|
+
// 3. Handle generic name prefixes (``d'``, ``van``, ``von``, ``de``,
|
|
27
|
+
// etc.): for each recognised prefix, emit ``(encoded_remainder)-
|
|
28
|
+
// (encoded_combined)`` and stop.
|
|
29
|
+
// 4. Walk the input. At each position, the rule set's Aho-Corasick
|
|
30
|
+
// trie finds the longest-prefix-matching pattern whose left and
|
|
31
|
+
// right context predicates also fire. Apply that rule's
|
|
32
|
+
// phoneme expression to the running PhonemeBuilder set, advance
|
|
33
|
+
// past the pattern. If no rule matches, drop the character.
|
|
34
|
+
// 5. Apply ``common`` final rules then language-specific final rules
|
|
35
|
+
// to convert language-conditional phonemes into a language-
|
|
36
|
+
// independent representation.
|
|
37
|
+
// 6. Emit ``|``-joined unique phonemes as UTF-8 (the BMPM phoneme
|
|
38
|
+
// alphabet contains Cyrillic / Greek / Polish glyphs).
|
|
39
|
+
//
|
|
40
|
+
// Performance:
|
|
41
|
+
// * One Aho-Corasick trie per ``(RuleType, language)`` shares a single
|
|
42
|
+
// scan over each position.
|
|
43
|
+
// * Left/right context predicates are pre-classified at static-init
|
|
44
|
+
// into ``ContextPredKind`` (empty, anchor, single char-class,
|
|
45
|
+
// literal prefix/suffix, anchored variants, regex-fallback).
|
|
46
|
+
// ``std::regex`` is only used for the few patterns the classifier
|
|
47
|
+
// rejects.
|
|
48
|
+
// * A per-call bump arena hosts the PhonemeBuilder set so the hot
|
|
49
|
+
// loop does not call ``malloc`` / ``free``.
|
|
50
|
+
// * The rule trie + final-rule maps + Lang regexes are parsed once
|
|
51
|
+
// into a single immutable ``BmpmTables`` singleton on the first
|
|
52
|
+
// ``beider_morse()`` call and reused thereafter (``std::call_once``
|
|
53
|
+
// -guarded).
|
|
54
|
+
//
|
|
55
|
+
// Source attribution:
|
|
56
|
+
// * Beider, A. & Morse, S.P. "Phonetic Matching: A Better Soundex"
|
|
57
|
+
// (2008). https://stevemorse.org/phonetics/bmpm.htm
|
|
58
|
+
// * Apache Commons Codec ``org.apache.commons.codec.language.bm.*``
|
|
59
|
+
// (Apache 2.0) for the algorithm structure and the rule data
|
|
60
|
+
// files. The vendored upstream rule files live in
|
|
61
|
+
// ``src/stride_align/bmpm_data/`` with their original ASF headers
|
|
62
|
+
// intact.
|
|
63
|
+
//
|
|
64
|
+
// The C++ port and the Aho-Corasick / arena / predicate-classifier
|
|
65
|
+
// machinery are original.
|
|
66
|
+
|
|
67
|
+
#include <cstddef>
|
|
68
|
+
#include <cstdint>
|
|
69
|
+
#include <string>
|
|
70
|
+
#include <unordered_map>
|
|
71
|
+
#include <vector>
|
|
72
|
+
|
|
73
|
+
namespace stride_align::phonetic {
|
|
74
|
+
|
|
75
|
+
// The engine works in codepoint space end-to-end. Python ``str`` storage
|
|
76
|
+
// is fixed-width per string (``PyUnicode_KIND`` returns 1/2/4 bytes per
|
|
77
|
+
// codepoint, ``PyUnicode_DATA`` is the raw codepoint array); the
|
|
78
|
+
// dispatch wrapper widens that storage straight into
|
|
79
|
+
// ``std::vector<Codepoint>`` without UTF-8 round-tripping on the input
|
|
80
|
+
// side. UTF-8 only appears on the output side (the returned phonetic
|
|
81
|
+
// codes) and inside the ``std::regex`` predicate fallback, which
|
|
82
|
+
// lazy-encodes the codepoint slice only when a regex predicate is
|
|
83
|
+
// about to fire.
|
|
84
|
+
using Codepoint = std::uint32_t;
|
|
85
|
+
|
|
86
|
+
enum class BmpmRuleType : int {
|
|
87
|
+
kApprox = 0,
|
|
88
|
+
kExact = 1,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Register the Apache Commons Codec BMPM resource files with the engine.
|
|
92
|
+
// ``resources`` maps the basename (e.g. ``"gen_rules_english"``, no
|
|
93
|
+
// ``.txt`` suffix) to the file's textual content. Safe to call multiple
|
|
94
|
+
// times — the first registration wins and the engine is built lazily on
|
|
95
|
+
// the first ``beider_morse()`` call after registration.
|
|
96
|
+
//
|
|
97
|
+
// Throws ``std::runtime_error`` if a required resource is missing or
|
|
98
|
+
// malformed.
|
|
99
|
+
void bmpm_register_resources(
|
|
100
|
+
const std::unordered_map<std::string, std::string>& resources);
|
|
101
|
+
|
|
102
|
+
// Encode ``input`` into one or more BMPM phonetic codes joined with
|
|
103
|
+
// ``|``. Returns an empty string when the engine has not been
|
|
104
|
+
// registered or when ``input`` contains no encodable characters.
|
|
105
|
+
//
|
|
106
|
+
// ``rule_type`` picks the rule family: ``kApprox`` produces a broader
|
|
107
|
+
// phonetic spread; ``kExact`` a tighter one.
|
|
108
|
+
//
|
|
109
|
+
// ``concat`` controls multi-word names: when true, the encoded form is
|
|
110
|
+
// produced for the concatenated whole; when false, each word is
|
|
111
|
+
// encoded separately and the per-word codes are joined with ``-``.
|
|
112
|
+
//
|
|
113
|
+
// ``max_phonemes`` caps the PhonemeBuilder set size (default 20,
|
|
114
|
+
// matching the upstream Commons Codec default).
|
|
115
|
+
std::string beider_morse(
|
|
116
|
+
const std::vector<Codepoint>& input,
|
|
117
|
+
BmpmRuleType rule_type = BmpmRuleType::kApprox,
|
|
118
|
+
bool concat = true,
|
|
119
|
+
std::size_t max_phonemes = 20);
|
|
120
|
+
|
|
121
|
+
} // namespace stride_align::phonetic
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// Caverphone 2.0 (David Hood, University of Otago, 2004).
|
|
4
|
+
//
|
|
5
|
+
// Phonetic encoder originally designed to match names from late-19th-
|
|
6
|
+
// century New Zealand electoral rolls; produces a fixed-length
|
|
7
|
+
// 10-character code right-padded with the digit ``1``. Caverphone 2
|
|
8
|
+
// supersedes the original 1.0 algorithm; we ship v2 only.
|
|
9
|
+
//
|
|
10
|
+
// The algorithm is a cascade of ~40 substitutions applied in a
|
|
11
|
+
// specific order. Order matters — most rules depend on the
|
|
12
|
+
// previous rules having already fired. The reference is Hood's
|
|
13
|
+
// "Caverphone Revisited" paper. We follow the rule list as
|
|
14
|
+
// reproduced in Apache Commons Codec Caverphone2.
|
|
15
|
+
//
|
|
16
|
+
// Sources:
|
|
17
|
+
// * Hood, D. "Caverphone Revisited." Technical Report, University
|
|
18
|
+
// of Otago, 2004.
|
|
19
|
+
// * Apache Commons Codec Caverphone2 class.
|
|
20
|
+
// * https://en.wikipedia.org/wiki/Caverphone
|
|
21
|
+
|
|
22
|
+
#include <cstddef>
|
|
23
|
+
#include <string>
|
|
24
|
+
#include <string_view>
|
|
25
|
+
|
|
26
|
+
namespace stride_align::phonetic {
|
|
27
|
+
|
|
28
|
+
namespace caverphone_detail {
|
|
29
|
+
|
|
30
|
+
inline constexpr char to_lower_ascii(char c) noexcept {
|
|
31
|
+
return (c >= 'A' && c <= 'Z') ? static_cast<char>(c + ('a' - 'A')) : c;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
inline constexpr bool is_lower_alpha(char c) noexcept {
|
|
35
|
+
return c >= 'a' && c <= 'z';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// In-place find/replace-all of a single substring.
|
|
39
|
+
inline void replace_all(std::string& s,
|
|
40
|
+
std::string_view from,
|
|
41
|
+
std::string_view to) {
|
|
42
|
+
if (from.empty()) return;
|
|
43
|
+
std::size_t pos = 0;
|
|
44
|
+
while ((pos = s.find(from, pos)) != std::string::npos) {
|
|
45
|
+
s.replace(pos, from.size(), to);
|
|
46
|
+
pos += to.size();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
inline bool starts_with(std::string_view s, std::string_view prefix) noexcept {
|
|
51
|
+
return s.size() >= prefix.size() &&
|
|
52
|
+
s.compare(0, prefix.size(), prefix) == 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
inline bool ends_with(std::string_view s, std::string_view suffix) noexcept {
|
|
56
|
+
return s.size() >= suffix.size() &&
|
|
57
|
+
s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
} // namespace caverphone_detail
|
|
61
|
+
|
|
62
|
+
inline std::string caverphone(std::string_view input) {
|
|
63
|
+
using namespace caverphone_detail;
|
|
64
|
+
|
|
65
|
+
// Step 0: lower-case ASCII only.
|
|
66
|
+
std::string w;
|
|
67
|
+
w.reserve(input.size());
|
|
68
|
+
for (char c : input) {
|
|
69
|
+
const char lc = to_lower_ascii(c);
|
|
70
|
+
if (is_lower_alpha(lc)) w.push_back(lc);
|
|
71
|
+
}
|
|
72
|
+
if (w.empty()) return std::string(10, '1');
|
|
73
|
+
|
|
74
|
+
// Initial "-ough" word replacements: only if the entire word starts
|
|
75
|
+
// with the listed prefix.
|
|
76
|
+
if (starts_with(w, "cough")) w.replace(0, 5, "cou2f");
|
|
77
|
+
else if (starts_with(w, "rough")) w.replace(0, 5, "rou2f");
|
|
78
|
+
else if (starts_with(w, "tough")) w.replace(0, 5, "tou2f");
|
|
79
|
+
else if (starts_with(w, "enough")) w.replace(0, 6, "enou2f");
|
|
80
|
+
else if (starts_with(w, "trough")) w.replace(0, 6, "trou2f");
|
|
81
|
+
|
|
82
|
+
// Initial gn -> 2n.
|
|
83
|
+
if (starts_with(w, "gn")) w.replace(0, 2, "2n");
|
|
84
|
+
// Final mb -> m2.
|
|
85
|
+
if (ends_with(w, "mb")) w.replace(w.size() - 2, 2, "m2");
|
|
86
|
+
|
|
87
|
+
// Plain substring substitutions (order-sensitive — each rule
|
|
88
|
+
// assumes the preceding ones have already fired).
|
|
89
|
+
replace_all(w, "cq", "2q");
|
|
90
|
+
replace_all(w, "ci", "si");
|
|
91
|
+
replace_all(w, "ce", "se");
|
|
92
|
+
replace_all(w, "cy", "sy");
|
|
93
|
+
replace_all(w, "tch", "2ch");
|
|
94
|
+
replace_all(w, "c", "k");
|
|
95
|
+
replace_all(w, "q", "k");
|
|
96
|
+
replace_all(w, "x", "k");
|
|
97
|
+
replace_all(w, "v", "f");
|
|
98
|
+
replace_all(w, "dg", "2g");
|
|
99
|
+
replace_all(w, "tio", "sio");
|
|
100
|
+
replace_all(w, "tia", "sia");
|
|
101
|
+
replace_all(w, "d", "t");
|
|
102
|
+
replace_all(w, "ph", "fh");
|
|
103
|
+
replace_all(w, "b", "p");
|
|
104
|
+
replace_all(w, "sh", "s2");
|
|
105
|
+
replace_all(w, "z", "s");
|
|
106
|
+
|
|
107
|
+
// Initial vowel -> A; remaining vowels -> 3.
|
|
108
|
+
if (!w.empty()) {
|
|
109
|
+
const char c0 = w.front();
|
|
110
|
+
if (c0 == 'a' || c0 == 'e' || c0 == 'i' || c0 == 'o' || c0 == 'u') {
|
|
111
|
+
w[0] = 'A';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (char& c : w) {
|
|
115
|
+
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') c = '3';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// GH context.
|
|
119
|
+
replace_all(w, "3gh3", "3kh3");
|
|
120
|
+
replace_all(w, "gh", "22");
|
|
121
|
+
replace_all(w, "g", "k");
|
|
122
|
+
|
|
123
|
+
// Consonant-run collapse: each rule maps consecutive same-letter
|
|
124
|
+
// sequences to a single upper-case marker, then back to lower.
|
|
125
|
+
// Order matters; we follow the published rule order.
|
|
126
|
+
auto collapse_runs = [&](char ch, char marker) {
|
|
127
|
+
// Replace runs of 2+ instances of ``ch`` with a single ``marker``.
|
|
128
|
+
std::string out;
|
|
129
|
+
out.reserve(w.size());
|
|
130
|
+
std::size_t i = 0;
|
|
131
|
+
while (i < w.size()) {
|
|
132
|
+
if (w[i] == ch) {
|
|
133
|
+
std::size_t j = i;
|
|
134
|
+
while (j < w.size() && w[j] == ch) ++j;
|
|
135
|
+
out.push_back(j - i >= 2 ? marker : ch);
|
|
136
|
+
i = j;
|
|
137
|
+
} else {
|
|
138
|
+
out.push_back(w[i]);
|
|
139
|
+
++i;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
w.swap(out);
|
|
143
|
+
for (char& c : w) {
|
|
144
|
+
if (c == marker) c = ch;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
collapse_runs('s', 'S');
|
|
148
|
+
collapse_runs('t', 'T');
|
|
149
|
+
collapse_runs('p', 'P');
|
|
150
|
+
collapse_runs('k', 'K');
|
|
151
|
+
collapse_runs('f', 'F');
|
|
152
|
+
collapse_runs('m', 'M');
|
|
153
|
+
collapse_runs('n', 'N');
|
|
154
|
+
|
|
155
|
+
// W rules.
|
|
156
|
+
replace_all(w, "w3", "W3");
|
|
157
|
+
replace_all(w, "wh3", "Wh3");
|
|
158
|
+
if (ends_with(w, "w")) w.back() = '3';
|
|
159
|
+
replace_all(w, "w", "2");
|
|
160
|
+
|
|
161
|
+
// H rules.
|
|
162
|
+
if (!w.empty() && w.front() == 'h') w[0] = 'A';
|
|
163
|
+
replace_all(w, "h", "2");
|
|
164
|
+
|
|
165
|
+
// R rules.
|
|
166
|
+
replace_all(w, "r3", "R3");
|
|
167
|
+
if (ends_with(w, "r")) w.back() = '3';
|
|
168
|
+
replace_all(w, "r", "2");
|
|
169
|
+
|
|
170
|
+
// L rules. The liquid L is preserved only when it sits between
|
|
171
|
+
// two vowel markers — "Cailean" (k33L33n) keeps L because both
|
|
172
|
+
// neighbours are 3; "hold" (A3lt) and "able" (Apl3) drop L
|
|
173
|
+
// because at least one neighbour is a consonant.
|
|
174
|
+
replace_all(w, "3l3", "3L3");
|
|
175
|
+
if (ends_with(w, "l")) w.back() = '3';
|
|
176
|
+
replace_all(w, "l", "2");
|
|
177
|
+
|
|
178
|
+
// J / Y rules.
|
|
179
|
+
replace_all(w, "j", "y");
|
|
180
|
+
if (starts_with(w, "y3")) w.replace(0, 2, "Y3");
|
|
181
|
+
if (!w.empty() && w.front() == 'y') w[0] = 'A';
|
|
182
|
+
replace_all(w, "y", "3");
|
|
183
|
+
|
|
184
|
+
// Strip 2s first, then handle the "trailing 3 → A" rule, then
|
|
185
|
+
// strip the remaining 3s. Order matters — the trailing 3 needs to
|
|
186
|
+
// survive past the 2-strip to be picked up here.
|
|
187
|
+
{
|
|
188
|
+
std::string out;
|
|
189
|
+
out.reserve(w.size());
|
|
190
|
+
for (char c : w) {
|
|
191
|
+
if (c != '2') out.push_back(c);
|
|
192
|
+
}
|
|
193
|
+
w.swap(out);
|
|
194
|
+
}
|
|
195
|
+
// Trailing 3 → A (gives names like "Peter" their final "A").
|
|
196
|
+
if (!w.empty() && w.back() == '3') w.back() = 'A';
|
|
197
|
+
// Strip remaining 3s.
|
|
198
|
+
{
|
|
199
|
+
std::string out;
|
|
200
|
+
out.reserve(w.size());
|
|
201
|
+
for (char c : w) {
|
|
202
|
+
if (c != '3') out.push_back(c);
|
|
203
|
+
}
|
|
204
|
+
w.swap(out);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Lower-case the marker letters back to plain ASCII.
|
|
208
|
+
for (char& c : w) {
|
|
209
|
+
if (c >= 'A' && c <= 'Z') c = static_cast<char>(c + ('a' - 'A'));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Pad with '1' to length 10, or truncate to 10.
|
|
213
|
+
if (w.size() >= 10) w.resize(10);
|
|
214
|
+
else w.append(10 - w.size(), '1');
|
|
215
|
+
// Upper-case for the final code (Apache Commons Codec convention).
|
|
216
|
+
for (char& c : w) {
|
|
217
|
+
if (c >= 'a' && c <= 'z') c = static_cast<char>(c - ('a' - 'A'));
|
|
218
|
+
}
|
|
219
|
+
return w;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
} // namespace stride_align::phonetic
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// Cologne Phonetic (Kölner Phonetik) — Hans Joachim Postel, 1969.
|
|
4
|
+
//
|
|
5
|
+
// German-language phonetic encoder. Maps letters / letter-pairs to
|
|
6
|
+
// digits 0-8 with context-sensitive rules for C, X, D, T, P; then
|
|
7
|
+
// collapses adjacent duplicate digits; then drops all '0' digits
|
|
8
|
+
// except a leading one. Result is a digit string of variable length.
|
|
9
|
+
//
|
|
10
|
+
// Designed for German names but widely applied to general European
|
|
11
|
+
// surnames. Distinguishes pairs like "Müller" / "Maier" that
|
|
12
|
+
// Soundex / Metaphone (English-oriented) tend to collapse the same.
|
|
13
|
+
//
|
|
14
|
+
// Sources:
|
|
15
|
+
// * Postel, H.-J. "Die Kölner Phonetik" (1969).
|
|
16
|
+
// * https://en.wikipedia.org/wiki/Cologne_phonetics
|
|
17
|
+
// * https://de.wikipedia.org/wiki/K%C3%B6lner_Phonetik
|
|
18
|
+
// * Apache Commons Codec ColognePhonetic class.
|
|
19
|
+
//
|
|
20
|
+
// Cross-checked against the Apache Commons Codec reference vectors
|
|
21
|
+
// and the Wikipedia examples (Wikipedia → 3412, Breschnew → 17863,
|
|
22
|
+
// Müller → 657).
|
|
23
|
+
|
|
24
|
+
#include <cstddef>
|
|
25
|
+
#include <cstdint>
|
|
26
|
+
#include <string>
|
|
27
|
+
#include <vector>
|
|
28
|
+
|
|
29
|
+
namespace stride_align::phonetic {
|
|
30
|
+
|
|
31
|
+
// The engine runs in codepoint space. Python ``str`` storage is
|
|
32
|
+
// fixed-width per string (``PyUnicode_KIND`` is 1/2/4 bytes per
|
|
33
|
+
// codepoint, ``PyUnicode_DATA`` is the raw codepoint array); the
|
|
34
|
+
// dispatch wrapper widens that storage straight into
|
|
35
|
+
// ``std::vector<Codepoint>`` without UTF-8 round-tripping.
|
|
36
|
+
using Codepoint = std::uint32_t;
|
|
37
|
+
|
|
38
|
+
namespace cologne_detail {
|
|
39
|
+
|
|
40
|
+
inline constexpr char to_upper_ascii(char c) noexcept {
|
|
41
|
+
return (c >= 'a' && c <= 'z') ? static_cast<char>(c - ('a' - 'A')) : c;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
inline constexpr bool is_upper_alpha(char c) noexcept {
|
|
45
|
+
return c >= 'A' && c <= 'Z';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Apache Commons Codec preprocesses the German umlauts and ß into
|
|
49
|
+
// their Latin-letter equivalents before running the digit table; we
|
|
50
|
+
// follow that so callers don't have to NFKD-fold themselves. The
|
|
51
|
+
// fold is now keyed on the codepoint (the natural Unicode value)
|
|
52
|
+
// rather than on UTF-8 byte pairs:
|
|
53
|
+
// Ä U+00C4 / ä U+00E4 -> A
|
|
54
|
+
// Ö U+00D6 / ö U+00F6 -> O
|
|
55
|
+
// Ü U+00DC / ü U+00FC -> U
|
|
56
|
+
// ß U+00DF -> SS
|
|
57
|
+
inline std::string preprocess_to_ascii_letters(
|
|
58
|
+
const std::vector<Codepoint>& input) {
|
|
59
|
+
std::string out;
|
|
60
|
+
out.reserve(input.size());
|
|
61
|
+
for (const auto cp : input) {
|
|
62
|
+
switch (cp) {
|
|
63
|
+
case 0x00C4: case 0x00E4: out.push_back('A'); continue; // Ä / ä
|
|
64
|
+
case 0x00D6: case 0x00F6: out.push_back('O'); continue; // Ö / ö
|
|
65
|
+
case 0x00DC: case 0x00FC: out.push_back('U'); continue; // Ü / ü
|
|
66
|
+
case 0x00DF: out.append("SS"); continue; // ß
|
|
67
|
+
default: break;
|
|
68
|
+
}
|
|
69
|
+
if (cp >= 128U) continue; // any other non-ASCII codepoint is dropped
|
|
70
|
+
const char uc = to_upper_ascii(static_cast<char>(cp));
|
|
71
|
+
if (is_upper_alpha(uc)) out.push_back(uc);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
} // namespace cologne_detail
|
|
77
|
+
|
|
78
|
+
inline std::string cologne_phonetic(const std::vector<Codepoint>& input) {
|
|
79
|
+
using namespace cologne_detail;
|
|
80
|
+
|
|
81
|
+
const std::string w = preprocess_to_ascii_letters(input);
|
|
82
|
+
if (w.empty()) return {};
|
|
83
|
+
|
|
84
|
+
const std::size_t n = w.size();
|
|
85
|
+
auto at = [&](std::size_t i) noexcept -> char {
|
|
86
|
+
return i < n ? w[i] : '\0';
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Step 1: emit digits per rule. H is silent (no digit), but it
|
|
90
|
+
// still counts as the previous letter for context. We track the
|
|
91
|
+
// previous SOURCE letter (not the previous emitted digit) so the
|
|
92
|
+
// "after S/Z" check for C looks at the right thing.
|
|
93
|
+
std::string codes;
|
|
94
|
+
codes.reserve(n);
|
|
95
|
+
for (std::size_t i = 0; i < n; ++i) {
|
|
96
|
+
const char ch = w[i];
|
|
97
|
+
const char next = at(i + 1);
|
|
98
|
+
const char prev = (i > 0) ? w[i - 1] : '\0';
|
|
99
|
+
|
|
100
|
+
switch (ch) {
|
|
101
|
+
case 'A': case 'E': case 'I': case 'J': case 'O': case 'U': case 'Y':
|
|
102
|
+
codes.push_back('0');
|
|
103
|
+
break;
|
|
104
|
+
case 'H':
|
|
105
|
+
// Silent — emit no digit. Postel's original rule: H is a
|
|
106
|
+
// "modifier" letter; it can appear in context lookups but
|
|
107
|
+
// never as an output digit on its own.
|
|
108
|
+
break;
|
|
109
|
+
case 'B':
|
|
110
|
+
codes.push_back('1');
|
|
111
|
+
break;
|
|
112
|
+
case 'P':
|
|
113
|
+
// P -> 3 before H ("Philip" -> 3...); P -> 1 elsewhere.
|
|
114
|
+
codes.push_back(next == 'H' ? '3' : '1');
|
|
115
|
+
break;
|
|
116
|
+
case 'D': case 'T':
|
|
117
|
+
// D/T -> 8 before C/S/Z (the affricate); -> 2 elsewhere.
|
|
118
|
+
codes.push_back(
|
|
119
|
+
(next == 'C' || next == 'S' || next == 'Z') ? '8' : '2');
|
|
120
|
+
break;
|
|
121
|
+
case 'F': case 'V': case 'W':
|
|
122
|
+
codes.push_back('3');
|
|
123
|
+
break;
|
|
124
|
+
case 'G': case 'K': case 'Q':
|
|
125
|
+
codes.push_back('4');
|
|
126
|
+
break;
|
|
127
|
+
case 'C': {
|
|
128
|
+
// C is the most context-sensitive letter. Rules:
|
|
129
|
+
// * Word-initial: -> 4 before A, H, K, L, O, Q, R, U, X;
|
|
130
|
+
// -> 8 otherwise.
|
|
131
|
+
// * Non-initial: -> 4 before A, H, K, O, Q, U, X
|
|
132
|
+
// and NOT immediately after S or Z;
|
|
133
|
+
// -> 8 otherwise.
|
|
134
|
+
// The L / R exception only applies at start of word.
|
|
135
|
+
const bool before_hard_initial =
|
|
136
|
+
next == 'A' || next == 'H' || next == 'K' || next == 'L' ||
|
|
137
|
+
next == 'O' || next == 'Q' || next == 'R' || next == 'U' ||
|
|
138
|
+
next == 'X';
|
|
139
|
+
const bool before_hard_inner =
|
|
140
|
+
next == 'A' || next == 'H' || next == 'K' || next == 'O' ||
|
|
141
|
+
next == 'Q' || next == 'U' || next == 'X';
|
|
142
|
+
if (i == 0) {
|
|
143
|
+
codes.push_back(before_hard_initial ? '4' : '8');
|
|
144
|
+
} else if (before_hard_inner && prev != 'S' && prev != 'Z') {
|
|
145
|
+
codes.push_back('4');
|
|
146
|
+
} else {
|
|
147
|
+
codes.push_back('8');
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case 'X':
|
|
152
|
+
// X -> 8 after C / K / Q (since the C/K/Q already emitted the
|
|
153
|
+
// /k/ part); X -> 48 elsewhere (representing /ks/).
|
|
154
|
+
if (prev == 'C' || prev == 'K' || prev == 'Q') {
|
|
155
|
+
codes.push_back('8');
|
|
156
|
+
} else {
|
|
157
|
+
codes.push_back('4');
|
|
158
|
+
codes.push_back('8');
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
case 'L':
|
|
162
|
+
codes.push_back('5');
|
|
163
|
+
break;
|
|
164
|
+
case 'M': case 'N':
|
|
165
|
+
codes.push_back('6');
|
|
166
|
+
break;
|
|
167
|
+
case 'R':
|
|
168
|
+
codes.push_back('7');
|
|
169
|
+
break;
|
|
170
|
+
case 'S': case 'Z':
|
|
171
|
+
codes.push_back('8');
|
|
172
|
+
break;
|
|
173
|
+
default:
|
|
174
|
+
// Already filtered to A-Z in preprocess_to_ascii_letters.
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Step 2: collapse adjacent duplicate digits.
|
|
180
|
+
std::string collapsed;
|
|
181
|
+
collapsed.reserve(codes.size());
|
|
182
|
+
for (char c : codes) {
|
|
183
|
+
if (collapsed.empty() || collapsed.back() != c) {
|
|
184
|
+
collapsed.push_back(c);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Step 3: drop all '0' digits except a leading one (when present).
|
|
189
|
+
// "Mayer" -> 607 -> after dropping internal zeros -> 67 (no leading
|
|
190
|
+
// 0 to preserve). "Aaron" -> 00706 -> collapse -> 0706 -> keep the
|
|
191
|
+
// leading 0, drop the rest -> 076.
|
|
192
|
+
std::string result;
|
|
193
|
+
result.reserve(collapsed.size());
|
|
194
|
+
for (std::size_t i = 0; i < collapsed.size(); ++i) {
|
|
195
|
+
if (i == 0 || collapsed[i] != '0') {
|
|
196
|
+
result.push_back(collapsed[i]);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
} // namespace stride_align::phonetic
|