Додано translit.js: транслітерація має жити поруч із фільтрами

Скелети, якими побудовано латинський шар фільтрів, мусять точно збігатися
з тими, що обчислює функція на вхідних словах. Тримаємо їх в одному пакеті,
щоб вони не розʼїхалися.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
full_droper
2026-09-19 12:32:08 +03:00
co-authored by Claude Opus 5
parent 0411edd826
commit 2ff3c9ae4a
2 changed files with 110 additions and 3 deletions
+8 -3
View File
@@ -1,9 +1,14 @@
{ {
"name": "cyrillic-lang-data", "name": "cyrillic-lang-data",
"version": "1.0.0", "version": "1.1.0",
"description": "Фільтри Блума на 8.6 млн словоформ 36 кириличних мов + таблиці літер, закінчень і триграм. Дані для ru-detector-v2.", "description": "Фільтри Блума на 8.6 млн словоформ 36 кириличних мов + таблиці літер, закінчень і триграм. Дані для ru-detector-v2.",
"type": "module", "type": "module",
"main": "index.js", "main": "index.js",
"license": "CC-BY-SA-4.0", "license": "CC-BY-SA-4.0",
"files": ["index.js", "signals.json", "filters"] "files": [
} "index.js",
"translit.js",
"signals.json",
"filters"
]
}
+102
View File
@@ -0,0 +1,102 @@
// Зведення слова до «скелета» — спільного вигляду для кирилиці та латинської транслітерації.
// Мета: щоб «привет», «privet», «privyet», «pryvjet» дали однаковий ключ.
// Використовується і при збиранні фільтрів, і в самій функції на вхідних словах.
// Латинські літери, якими підміняють кириличні, щоб обійти детектор (гомогліфи).
const HOMOGLYPHS = {
a: 'а', b: 'ь', c: 'с', e: 'е', h: 'н', i: 'і', j: 'ј', k: 'к', m: 'м',
o: 'о', p: 'р', s: 'ѕ', t: 'т', x: 'х', y: 'у', A: 'А', B: 'В', C: 'С',
E: 'Е', H: 'Н', I: 'І', K: 'К', M: 'М', O: 'О', P: 'Р', T: 'Т', X: 'Х', Y: 'У'
};
// Кирилиця → латинський скелет. Масив = неоднозначний запис, дає кілька варіантів.
const CYR = {
а: 'a', б: 'b', в: 'v', г: ['g', 'h'], ґ: 'g', д: 'd', е: 'e', є: 'ie', ё: ['e', 'o'],
ж: 'Z', з: 'z', и: 'i', і: 'i', ї: 'i', й: 'i', к: 'k', л: 'l', м: 'm', н: 'n',
о: 'o', п: 'p', р: 'r', с: 's', т: 't', у: 'u', ў: 'u', ф: 'f', х: 'h', ц: 'c',
ч: 'C', ш: 'S', щ: ['S', 'Sc'], ъ: '', ы: 'i', ь: '', э: 'e', ю: 'iu', я: 'ia',
// сербська, македонська
ђ: 'dj', ј: 'i', љ: 'l', њ: 'n', ћ: 'c', џ: 'dZ', ѓ: 'g', ќ: 'k', ѕ: 'z',
// тюркські, кавказькі, монгольська
ә: 'a', ғ: 'g', қ: 'k', ң: 'n', ө: 'o', ұ: 'u', ү: 'u', һ: 'h', ӣ: 'i', ӯ: 'u',
ҳ: 'h', ҷ: 'C', ҹ: 'C', җ: 'Z', ӑ: 'a', ӗ: 'e', ҫ: 's', ӳ: 'u', ӧ: 'o', ӱ: 'u',
ӹ: 'i', ӏ: '', ҕ: 'g', ҥ: 'n', ө: 'o', ӊ: 'n', ԥ: 'p', ҍ: '', ҩ: 'o'
};
// Цифри замість літер — теж спосіб обійти детектор: «4то», «3ачем».
const LEET = { 4: 'ch', 3: 'z', 0: 'o', 1: 'i', 6: 'b', 5: 's', 9: 'ya' };
// Латиниця → той самий скелет. Порядок важливий: спершу диграфи.
const LAT_RULES = [
[/shch|shtch|sch|szcz/g, 'S'],
[/zh|jh/g, 'Z'],
[/ch|tsch|cz/g, 'C'],
[/sh|sz/g, 'S'],
[/kh/g, 'h'],
[/ts|tc|tz/g, 'c'],
[/ya|ja/g, 'ia'],
[/yu|ju/g, 'iu'],
[/ye|je/g, 'e'], // є і е зводимо до одного
[/yo|jo/g, 'o'], // ё найчастіше пишуть як o
[/yi|ji|yy/g, 'i'],
[/gh/g, 'g'],
[/ck/g, 'k'],
[/[wq]/g, (m) => (m === 'w' ? 'v' : 'k')],
[/x/g, 'h'],
[/[yj]/g, 'i']
];
const hasCyr = (s) => /[Ѐ-ӿ]/.test(s);
// Прибирає подвоєння та службові знаки, зводить «ie» до «e» (є ≡ е).
function tidy(s) {
return s
.replace(/[^a-zA-Z]/g, '')
.replace(/(.)\1+/g, '$1')
.replace(/ie/g, 'e')
.replace(/(.)\1+/g, '$1');
}
// Кирилиця → масив варіантів скелета (обмежуємо, щоб не розростався).
function fromCyrillic(word) {
let variants = [''];
for (const ch of word) {
const map = CYR[ch];
if (map === undefined) continue; // цифри, знаки, латинські вкраплення
const options = Array.isArray(map) ? map : [map];
const next = [];
for (const v of variants) for (const o of options) next.push(v + o);
variants = next.length > 8 ? next.slice(0, 8) : next;
}
return variants;
}
function fromLatin(word) {
const base = word.toLowerCase().replace(/[0-9]/g, (d) => LEET[d] ?? '');
// «j» читають і як «й» (ja = я), і як «ж» (jizn = жизнь) — беремо обидва прочитання.
const branches = base.includes('j') ? [base, base.replace(/j/g, 'Z')] : [base];
return branches.map((b) => {
let s = b;
for (const [re, to] of LAT_RULES) s = s.replace(re, to);
return s;
});
}
// Якщо слово змішане і кирилиці в ньому більше — латинські гомогліфи вважаємо підміною.
export function deHomoglyph(word) {
const cyr = (word.match(/[Ѐ-ӿ]/g) || []).length;
const lat = (word.match(/[a-zA-Z]/g) || []).length;
if (cyr === 0 || lat === 0 || cyr < lat) return word;
return word.replace(/[a-zA-Z]/g, (c) => HOMOGLYPHS[c] || c);
}
// Головна функція: слово → масив скелетів (зазвичай один).
export function skeletons(word) {
const w = deHomoglyph(word.toLowerCase().replace(/[̀-ͯ҃-҉]/g, ''));
const raw = hasCyr(w) ? fromCyrillic(w) : fromLatin(w);
return [...new Set(raw.map(tidy).filter((s) => s.length > 1))];
}
export function skeleton(word) {
return skeletons(word)[0] || '';
}