baburchi 1.7.2 โ 1.7.3
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.
- package/README.md +4 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
|
|
15
15
|
A lightweight TypeScript library for intelligent OCR text post-processing, specializing in Arabic text with advanced typo correction using sequence alignment algorithms and comprehensive noise detection.
|
|
16
16
|
|
|
17
|
+
## Demo
|
|
18
|
+
|
|
19
|
+
Explore the interactive demo at <https://baburchi.surge.sh> to browse each exported helper, try Arabic-aware examples, and see formatting results in real time. The demo build ships with a `public/CNAME` file to keep the Surge domain in sync with deployments.
|
|
20
|
+
|
|
17
21
|
## Features
|
|
18
22
|
|
|
19
23
|
- ๐ง **Sequence-Aware Typo Repair** — NeedlemanโWunsch alignment with typo symbol preservation and duplicate pruning.
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["PRESETS: Record<SanitizePreset, PresetOptions>","PRESET_NONE: PresetOptions","outCode","preset: PresetOptions","opts: SanitizeOptions | null","results: string[]","alignment: AlignedTokenPair[]","matrix: AlignmentCell[][]","alignmentScore: number","alignedLines: string[]","errors: BalanceError[]","isBalanced","stack: Array<{ char: string; index: number }>","characterErrors: CharacterError[]","lookup: { [key: string]: string }","q: number[]","DEFAULT_POLICY: Required<MatchPolicy>","parts: string[]","starts: number[]","lens: number[]","patIdToOrigIdxs: number[][]","patterns: string[]","bits: string[]","windows: string[]","best: number | null","items: GramItem[]","result: GramBase[]","seams: SeamData[]","candidates: Candidate[]","best: FuzzyMatch | null","exact: [number, PageHit][]","fuzzy: [number, PageHit][]","hitsByExcerpt: Array<Map<number, PageHit>>","stats: CharacterStats","result: string[]"],"sources":["../src/utils/sanitize.ts","../src/utils/levenshthein.ts","../src/utils/similarity.ts","../src/alignment.ts","../src/balance.ts","../src/utils/textUtils.ts","../src/footnotes.ts","../src/utils/ahocorasick.ts","../src/utils/constants.ts","../src/utils/fuzzyUtils.ts","../src/utils/qgram.ts","../src/fuzzy.ts","../src/noise.ts","../src/typos.ts"],"sourcesContent":["/**\n * Ultra-fast Arabic text sanitizer for search/indexing/display.\n * Optimized for very high call rates: avoids per-call object spreads and minimizes allocations.\n * Options can merge over a base preset or `'none'` to apply exactly the rules you request.\n */\nexport type SanitizePreset = 'light' | 'search' | 'aggressive';\nexport type SanitizeBase = 'none' | SanitizePreset;\n\n/**\n * Public options for {@link sanitizeArabic}. When you pass an options object, it overlays the chosen\n * `base` (default `'light'`) without allocating merged objects on the hot path; flags are resolved\n * directly into local booleans for speed.\n */\nexport type SanitizeOptions = {\n /** Base to merge over. `'none'` applies only the options you specify. Default when passing an object: `'light'`. */\n base?: SanitizeBase;\n\n /**\n * NFC normalization (fast-path).\n *\n * For performance, this sanitizer avoids calling `String.prototype.normalize('NFC')` and instead\n * applies the key Arabic canonical compositions inline (hamza/madda combining marks).\n * This preserves the NFC behavior that matters for typical Arabic OCR text while keeping throughput high.\n *\n * Default: `true` in all presets.\n */\n nfc?: boolean;\n\n /** Strip zero-width controls (U+200BโU+200F, U+202AโU+202E, U+2060โU+2064, U+FEFF). Default: `true` in presets. */\n stripZeroWidth?: boolean;\n\n /** If stripping zero-width, replace them with a space instead of removing. Default: `false`. */\n zeroWidthToSpace?: boolean;\n\n /** Remove Arabic diacritics (tashkฤซl). Default: `true` in `'search'`/`'aggressive'`. */\n stripDiacritics?: boolean;\n\n /** Remove footnote references. Default: `true` in `'search'`/`'aggressive'`. */\n stripFootnotes?: boolean;\n\n /**\n * Remove tatweel (ู).\n * - `true` is treated as `'safe'` (preserves tatweel after digits or 'ู' for dates/list markers)\n * - `'safe'` or `'all'` explicitly\n * - `false` to keep tatweel\n * Default: `'all'` in `'search'`/`'aggressive'`, `false` in `'light'`.\n */\n stripTatweel?: boolean | 'safe' | 'all';\n\n /** Normalize ุข/ุฃ/ุฅ โ ุง. Default: `true` in `'search'`/`'aggressive'`. */\n normalizeAlif?: boolean;\n\n /** Replace ู โ ู. Default: `true` in `'search'`/`'aggressive'`. */\n replaceAlifMaqsurah?: boolean;\n\n /** Replace ุฉ โ ู (lossy). Default: `true` in `'aggressive'` only. */\n replaceTaMarbutahWithHa?: boolean;\n\n /** Strip Latin letters/digits and common OCR noise into spaces. Default: `true` in `'aggressive'`. */\n stripLatinAndSymbols?: boolean;\n\n /** Keep only Arabic letters (no whitespace). Use for compact keys, not FTS. */\n keepOnlyArabicLetters?: boolean;\n\n /** Keep Arabic letters + spaces (drops digits/punct/symbols). Great for FTS. Default: `true` in `'aggressive'`. */\n lettersAndSpacesOnly?: boolean;\n\n /** Collapse runs of whitespace to a single space. Default: `true`. */\n collapseWhitespace?: boolean;\n\n /** Trim leading/trailing whitespace. Default: `true`. */\n trim?: boolean;\n\n /**\n * Remove the Hijri date marker (\"ูู\" or bare \"ู\" if tatweel already removed) when it follows a date-like token\n * (digits/slashes/hyphens/spaces). Example: `1435/3/29 ูู` โ `1435/3/29`.\n * Default: `true` in `'search'`/`'aggressive'`, `false` in `'light'`.\n */\n removeHijriMarker?: boolean;\n};\n\n/** Fully-resolved internal preset options (no `base`, and tatweel as a mode). */\ntype PresetOptions = {\n nfc: boolean;\n stripZeroWidth: boolean;\n zeroWidthToSpace: boolean;\n stripDiacritics: boolean;\n stripFootnotes: boolean;\n stripTatweel: false | 'safe' | 'all';\n normalizeAlif: boolean;\n replaceAlifMaqsurah: boolean;\n replaceTaMarbutahWithHa: boolean;\n stripLatinAndSymbols: boolean;\n keepOnlyArabicLetters: boolean;\n lettersAndSpacesOnly: boolean;\n collapseWhitespace: boolean;\n trim: boolean;\n removeHijriMarker: boolean;\n};\n\n/** Fully-resolved internal options with short names for performance. */\ntype ResolvedOptions = {\n nfc: boolean;\n stripZW: boolean;\n zwAsSpace: boolean;\n removeHijri: boolean;\n removeDia: boolean;\n tatweelMode: false | 'safe' | 'all';\n normAlif: boolean;\n maqToYa: boolean;\n taToHa: boolean;\n removeFootnotes: boolean;\n lettersSpacesOnly: boolean;\n stripNoise: boolean;\n lettersOnly: boolean;\n collapseWS: boolean;\n doTrim: boolean;\n};\n\nconst PRESETS: Record<SanitizePreset, PresetOptions> = {\n aggressive: {\n collapseWhitespace: true,\n keepOnlyArabicLetters: false,\n lettersAndSpacesOnly: true,\n nfc: true,\n normalizeAlif: true,\n removeHijriMarker: true,\n replaceAlifMaqsurah: true,\n replaceTaMarbutahWithHa: true,\n stripDiacritics: true,\n stripFootnotes: true,\n stripLatinAndSymbols: true,\n stripTatweel: 'all',\n stripZeroWidth: true,\n trim: true,\n zeroWidthToSpace: false,\n },\n light: {\n collapseWhitespace: true,\n keepOnlyArabicLetters: false,\n lettersAndSpacesOnly: false,\n nfc: true,\n normalizeAlif: false,\n removeHijriMarker: false,\n replaceAlifMaqsurah: false,\n replaceTaMarbutahWithHa: false,\n stripDiacritics: false,\n stripFootnotes: false,\n stripLatinAndSymbols: false,\n stripTatweel: false,\n stripZeroWidth: true,\n trim: true,\n zeroWidthToSpace: false,\n },\n search: {\n collapseWhitespace: true,\n keepOnlyArabicLetters: false,\n lettersAndSpacesOnly: false,\n nfc: true,\n normalizeAlif: true,\n removeHijriMarker: true,\n replaceAlifMaqsurah: true,\n replaceTaMarbutahWithHa: false,\n stripDiacritics: true,\n stripFootnotes: true,\n stripLatinAndSymbols: false,\n stripTatweel: 'all',\n stripZeroWidth: true,\n trim: true,\n zeroWidthToSpace: false,\n },\n} as const;\n\nconst PRESET_NONE: PresetOptions = {\n collapseWhitespace: false,\n keepOnlyArabicLetters: false,\n lettersAndSpacesOnly: false,\n nfc: false,\n normalizeAlif: false,\n removeHijriMarker: false,\n replaceAlifMaqsurah: false,\n replaceTaMarbutahWithHa: false,\n stripDiacritics: false,\n stripFootnotes: false,\n stripLatinAndSymbols: false,\n stripTatweel: false,\n stripZeroWidth: false,\n trim: false,\n zeroWidthToSpace: false,\n};\n\n// Constants for character codes\nconst CHAR_SPACE = 32;\nconst CHAR_TATWEEL = 0x0640;\nconst CHAR_HA = 0x0647;\nconst CHAR_YA = 0x064a;\nconst CHAR_WAW = 0x0648;\nconst CHAR_ALIF = 0x0627;\nconst CHAR_ALIF_MADDA = 0x0622;\nconst CHAR_ALIF_HAMZA_ABOVE = 0x0623;\nconst CHAR_WAW_HAMZA_ABOVE = 0x0624;\nconst CHAR_ALIF_HAMZA_BELOW = 0x0625;\nconst CHAR_YEH_HAMZA_ABOVE = 0x0626;\nconst CHAR_ALIF_WASLA = 0x0671;\nconst CHAR_ALIF_MAQSURAH = 0x0649;\nconst CHAR_TA_MARBUTAH = 0x0629;\nconst CHAR_MADDA_ABOVE = 0x0653;\nconst CHAR_HAMZA_ABOVE_MARK = 0x0654;\nconst CHAR_HAMZA_BELOW_MARK = 0x0655;\n\n// Shared resources to avoid allocations\nlet sharedBuffer = new Uint16Array(2048); // Start with 2KB (enough for ~1000 chars)\nconst decoder = new TextDecoder('utf-16le');\n\n// Diacritic ranges\nconst isDiacritic = (code: number): boolean => {\n return (\n (code >= 0x064b && code <= 0x065f) ||\n (code >= 0x0610 && code <= 0x061a) ||\n code === 0x0670 ||\n (code >= 0x06d6 && code <= 0x06ed)\n );\n};\n\nconst isZeroWidth = (code: number): boolean => {\n return (\n (code >= 0x200b && code <= 0x200f) ||\n (code >= 0x202a && code <= 0x202e) ||\n (code >= 0x2060 && code <= 0x2064) ||\n code === 0xfeff\n );\n};\n\nconst isLatinOrDigit = (code: number): boolean => {\n return (\n (code >= 65 && code <= 90) || // A-Z\n (code >= 97 && code <= 122) || // a-z\n (code >= 48 && code <= 57) // 0-9\n );\n};\n\nconst isSymbol = (code: number): boolean => {\n // [ยฌยง`=]|[&]|[๏ทบ]\n return (\n code === 0x00ac || // ยฌ\n code === 0x00a7 || // ยง\n code === 0x0060 || // `\n code === 0x003d || // =\n code === 0x0026 || // &\n code === 0xfdfa // ๏ทบ\n );\n};\n\nconst isArabicLetter = (code: number): boolean => {\n return (\n (code >= 0x0621 && code <= 0x063a) ||\n (code >= 0x0641 && code <= 0x064a) ||\n code === 0x0671 ||\n code === 0x067e ||\n code === 0x0686 ||\n (code >= 0x06a4 && code <= 0x06af) ||\n code === 0x06cc ||\n code === 0x06d2 ||\n code === 0x06d3\n );\n};\n\n/**\n * Checks whether a code point represents a Western or Arabic-Indic digit.\n *\n * @param code - The numeric code point to evaluate.\n * @returns True when the code point is a digit in either numeral system.\n */\nconst isDigit = (code: number): boolean => (code >= 48 && code <= 57) || (code >= 0x0660 && code <= 0x0669);\n\n/**\n * Resolves a boolean by taking an optional override over a preset value.\n *\n * @param presetValue - The value defined by the preset.\n * @param override - Optional override provided by the caller.\n * @returns The resolved boolean value.\n */\nconst resolveBoolean = (presetValue: boolean, override?: boolean): boolean =>\n override === undefined ? presetValue : !!override;\n\n/**\n * Resolves the tatweel mode by taking an optional override over a preset mode.\n * An override of `true` maps to `'safe'` for convenience.\n *\n * @param presetValue - The mode specified by the preset.\n * @param override - Optional override provided by the caller.\n * @returns The resolved tatweel mode.\n */\nconst resolveTatweelMode = (\n presetValue: false | 'safe' | 'all',\n override?: boolean | 'safe' | 'all',\n): false | 'safe' | 'all' => {\n if (override === undefined) {\n return presetValue;\n }\n if (override === true) {\n return 'safe';\n }\n if (override === false) {\n return false;\n }\n return override;\n};\n\n/**\n * Internal sanitization logic that applies all transformations to a single string.\n * Uses single-pass character transformation for maximum performance when possible.\n * This function assumes all options have been pre-resolved for maximum performance.\n */\nconst applySanitization = (input: string, options: ResolvedOptions): string => {\n if (!input) {\n return '';\n }\n\n const {\n nfc,\n stripZW,\n zwAsSpace,\n removeHijri,\n removeDia,\n tatweelMode,\n normAlif,\n maqToYa,\n taToHa,\n removeFootnotes,\n lettersSpacesOnly,\n stripNoise,\n lettersOnly,\n collapseWS,\n doTrim,\n } = options;\n\n /**\n * NFC Normalization (Fast Path)\n *\n * `String.prototype.normalize('NFC')` is extremely expensive under high throughput.\n * For Arabic OCR text, the main canonical compositions we care about are:\n * - ุง + โู (U+0653) โ ุข\n * - ุง + โู (U+0654) โ ุฃ\n * - ุง + โู (U+0655) โ ุฅ\n * - ู + โู (U+0654) โ ุค\n * - ู + โู (U+0654) โ ุฆ\n *\n * We implement these compositions inline during the main loop, avoiding full NFC\n * normalization in the common case while preserving behavior needed by our sanitizer.\n */\n const text = input;\n const len = text.length;\n\n // Ensure shared buffer is large enough\n if (len > sharedBuffer.length) {\n sharedBuffer = new Uint16Array(len + 1024);\n }\n const buffer = sharedBuffer;\n let bufIdx = 0;\n\n let lastWasSpace = false;\n\n // Skip leading whitespace if trimming\n let start = 0;\n if (doTrim) {\n while (start < len && text.charCodeAt(start) <= 32) {\n start++;\n }\n }\n\n for (let i = start; i < len; i++) {\n const code = text.charCodeAt(i);\n\n // Whitespace handling\n if (code <= 32) {\n if (lettersOnly) {\n continue; // Drop spaces if lettersOnly\n }\n\n if (collapseWS) {\n if (!lastWasSpace && bufIdx > 0) {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = true;\n }\n } else {\n buffer[bufIdx++] = CHAR_SPACE; // Normalize to space\n lastWasSpace = false;\n }\n continue;\n }\n\n // NFC (subset) for Arabic canonical compositions: merge combining marks into previous output\n if (nfc) {\n if (code === CHAR_MADDA_ABOVE || code === CHAR_HAMZA_ABOVE_MARK || code === CHAR_HAMZA_BELOW_MARK) {\n const prevIdx = bufIdx - 1;\n if (prevIdx >= 0) {\n const prev = buffer[prevIdx];\n let composed = 0;\n\n if (prev === CHAR_ALIF) {\n if (code === CHAR_MADDA_ABOVE) {\n composed = CHAR_ALIF_MADDA;\n } else if (code === CHAR_HAMZA_ABOVE_MARK) {\n composed = CHAR_ALIF_HAMZA_ABOVE;\n } else {\n // CHAR_HAMZA_BELOW_MARK\n composed = CHAR_ALIF_HAMZA_BELOW;\n }\n } else if (code === CHAR_HAMZA_ABOVE_MARK) {\n // Only Hamza Above composes for WAW/YEH in NFC\n if (prev === CHAR_WAW) {\n composed = CHAR_WAW_HAMZA_ABOVE;\n } else if (prev === CHAR_YA) {\n composed = CHAR_YEH_HAMZA_ABOVE;\n }\n }\n\n if (composed !== 0) {\n buffer[prevIdx] = composed;\n continue;\n }\n }\n }\n }\n\n // Zero width\n if (stripZW && isZeroWidth(code)) {\n if (zwAsSpace) {\n if (collapseWS) {\n if (!lastWasSpace && bufIdx > 0) {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = true;\n }\n } else {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = false;\n }\n }\n continue;\n }\n\n // Hijri Marker Removal (Must run before letter filtering removes digits)\n if (removeHijri && code === CHAR_HA) {\n let nextIdx = i + 1;\n if (nextIdx < len && text.charCodeAt(nextIdx) === CHAR_TATWEEL) {\n nextIdx++;\n }\n\n let isBoundary = false;\n if (nextIdx >= len) {\n isBoundary = true;\n } else {\n const nextCode = text.charCodeAt(nextIdx);\n if (nextCode <= 32 || isSymbol(nextCode) || nextCode === 47 || nextCode === 45) {\n isBoundary = true;\n }\n }\n\n if (isBoundary) {\n let backIdx = i - 1;\n while (backIdx >= 0) {\n const c = text.charCodeAt(backIdx);\n if (c <= 32 || isZeroWidth(c)) {\n backIdx--;\n } else {\n break;\n }\n }\n if (backIdx >= 0 && isDigit(text.charCodeAt(backIdx))) {\n if (nextIdx > i + 1) {\n i++;\n }\n continue;\n }\n }\n }\n\n // Diacritics\n if (removeDia && isDiacritic(code)) {\n continue;\n }\n\n // Tatweel\n if (code === CHAR_TATWEEL) {\n if (tatweelMode === 'all') {\n continue;\n }\n if (tatweelMode === 'safe') {\n let backIdx = bufIdx - 1;\n while (backIdx >= 0 && buffer[backIdx] === CHAR_SPACE) {\n backIdx--;\n }\n if (backIdx >= 0) {\n const prev = buffer[backIdx];\n if (isDigit(prev) || prev === CHAR_HA) {\n // Keep it\n } else {\n continue; // Drop\n }\n } else {\n continue; // Drop\n }\n }\n }\n\n // Latin and Symbols (Skip if letter filtering will handle it)\n if (stripNoise && !lettersSpacesOnly && !lettersOnly) {\n if (isLatinOrDigit(code) || isSymbol(code)) {\n if (collapseWS) {\n if (!lastWasSpace && bufIdx > 0) {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = true;\n }\n } else {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = false;\n }\n continue;\n }\n // Double slash check //\n if (code === 47 && i + 1 < len && text.charCodeAt(i + 1) === 47) {\n while (i + 1 < len && text.charCodeAt(i + 1) === 47) {\n i++;\n }\n if (collapseWS) {\n if (!lastWasSpace && bufIdx > 0) {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = true;\n }\n } else {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = false;\n }\n continue;\n }\n }\n\n // Footnote Removal (Skip if letter filtering will handle it)\n if (removeFootnotes && !lettersSpacesOnly && !lettersOnly && code === 40) {\n // (\n let nextIdx = i + 1;\n if (nextIdx < len && text.charCodeAt(nextIdx) === CHAR_SPACE) {\n nextIdx++;\n }\n\n if (nextIdx < len) {\n const c1 = text.charCodeAt(nextIdx);\n\n // Pattern 1: (ยฌ123...)\n if (c1 === 0x00ac) {\n // ยฌ\n nextIdx++;\n let hasDigits = false;\n while (nextIdx < len) {\n const c = text.charCodeAt(nextIdx);\n if (c >= 0x0660 && c <= 0x0669) {\n hasDigits = true;\n nextIdx++;\n } else {\n break;\n }\n }\n if (hasDigits && nextIdx < len) {\n if (text.charCodeAt(nextIdx) === 41) {\n // )\n i = nextIdx;\n if (collapseWS) {\n if (!lastWasSpace && bufIdx > 0) {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = true;\n }\n } else {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = false;\n }\n continue;\n }\n if (text.charCodeAt(nextIdx) === CHAR_SPACE) {\n nextIdx++;\n if (nextIdx < len && text.charCodeAt(nextIdx) === 41) {\n i = nextIdx;\n if (collapseWS) {\n if (!lastWasSpace && bufIdx > 0) {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = true;\n }\n } else {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = false;\n }\n continue;\n }\n }\n }\n }\n\n // Pattern 2: (1) or (1 X)\n else if (c1 >= 0x0660 && c1 <= 0x0669) {\n let tempIdx = nextIdx + 1;\n let matched = false;\n\n if (tempIdx < len) {\n const c2 = text.charCodeAt(tempIdx);\n if (c2 === 41) {\n // )\n matched = true;\n tempIdx++;\n } else if (c2 === CHAR_SPACE) {\n // Space\n tempIdx++;\n if (tempIdx < len) {\n const c3 = text.charCodeAt(tempIdx);\n if (c3 >= 0x0600 && c3 <= 0x06ff) {\n tempIdx++;\n if (tempIdx < len && text.charCodeAt(tempIdx) === 41) {\n matched = true;\n tempIdx++;\n }\n }\n }\n }\n }\n\n if (matched) {\n i = tempIdx - 1;\n if (collapseWS) {\n if (!lastWasSpace && bufIdx > 0) {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = true;\n }\n } else {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = false;\n }\n continue;\n }\n }\n }\n }\n\n // Letter Filtering (Aggressive)\n if (lettersSpacesOnly || lettersOnly) {\n if (!isArabicLetter(code)) {\n if (lettersOnly) {\n continue;\n }\n // lettersSpacesOnly -> replace with space\n if (collapseWS) {\n if (!lastWasSpace && bufIdx > 0) {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = true;\n }\n } else {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = false;\n }\n continue;\n }\n\n // Normalization logic duplicated for speed\n let outCode = code;\n if (normAlif) {\n if (\n code === CHAR_ALIF_MADDA ||\n code === CHAR_ALIF_HAMZA_ABOVE ||\n code === CHAR_ALIF_HAMZA_BELOW ||\n code === CHAR_ALIF_WASLA\n ) {\n outCode = CHAR_ALIF;\n }\n }\n if (maqToYa && code === CHAR_ALIF_MAQSURAH) {\n outCode = CHAR_YA;\n }\n if (taToHa && code === CHAR_TA_MARBUTAH) {\n outCode = CHAR_HA;\n }\n\n buffer[bufIdx++] = outCode;\n lastWasSpace = false;\n continue;\n }\n\n // Normalization\n let outCode = code;\n if (normAlif) {\n if (\n code === CHAR_ALIF_MADDA ||\n code === CHAR_ALIF_HAMZA_ABOVE ||\n code === CHAR_ALIF_HAMZA_BELOW ||\n code === CHAR_ALIF_WASLA\n ) {\n outCode = CHAR_ALIF;\n }\n }\n if (maqToYa && code === CHAR_ALIF_MAQSURAH) {\n outCode = CHAR_YA;\n }\n if (taToHa && code === CHAR_TA_MARBUTAH) {\n outCode = CHAR_HA;\n }\n\n buffer[bufIdx++] = outCode;\n lastWasSpace = false;\n }\n\n // Trailing trim\n if (doTrim && lastWasSpace && bufIdx > 0) {\n bufIdx--;\n }\n\n if (bufIdx === 0) {\n return '';\n }\n const resultView = buffer.subarray(0, bufIdx);\n return decoder.decode(resultView);\n};\n\n/**\n * Resolves options from a preset or custom options object.\n * Returns all resolved flags for reuse in batch processing.\n */\nconst resolveOptions = (optionsOrPreset: SanitizePreset | SanitizeOptions): ResolvedOptions => {\n let preset: PresetOptions;\n let opts: SanitizeOptions | null = null;\n\n if (typeof optionsOrPreset === 'string') {\n preset = PRESETS[optionsOrPreset];\n } else {\n const base = optionsOrPreset.base ?? 'light';\n preset = base === 'none' ? PRESET_NONE : PRESETS[base];\n opts = optionsOrPreset;\n }\n\n return {\n collapseWS: resolveBoolean(preset.collapseWhitespace, opts?.collapseWhitespace),\n doTrim: resolveBoolean(preset.trim, opts?.trim),\n lettersOnly: resolveBoolean(preset.keepOnlyArabicLetters, opts?.keepOnlyArabicLetters),\n lettersSpacesOnly: resolveBoolean(preset.lettersAndSpacesOnly, opts?.lettersAndSpacesOnly),\n maqToYa: resolveBoolean(preset.replaceAlifMaqsurah, opts?.replaceAlifMaqsurah),\n nfc: resolveBoolean(preset.nfc, opts?.nfc),\n normAlif: resolveBoolean(preset.normalizeAlif, opts?.normalizeAlif),\n removeDia: resolveBoolean(preset.stripDiacritics, opts?.stripDiacritics),\n removeFootnotes: resolveBoolean(preset.stripFootnotes, opts?.stripFootnotes),\n removeHijri: resolveBoolean(preset.removeHijriMarker, opts?.removeHijriMarker),\n stripNoise: resolveBoolean(preset.stripLatinAndSymbols, opts?.stripLatinAndSymbols),\n stripZW: resolveBoolean(preset.stripZeroWidth, opts?.stripZeroWidth),\n taToHa: resolveBoolean(preset.replaceTaMarbutahWithHa, opts?.replaceTaMarbutahWithHa),\n tatweelMode: resolveTatweelMode(preset.stripTatweel, opts?.stripTatweel),\n zwAsSpace: resolveBoolean(preset.zeroWidthToSpace, opts?.zeroWidthToSpace),\n };\n};\n\n/**\n * Creates a reusable sanitizer function with pre-resolved options.\n * Use this when you need to sanitize many strings with the same options\n * for maximum performance.\n *\n * @example\n * ```ts\n * const sanitize = createArabicSanitizer('search');\n * const results = texts.map(sanitize);\n * ```\n */\nexport const createArabicSanitizer = (\n optionsOrPreset: SanitizePreset | SanitizeOptions = 'search',\n): ((input: string) => string) => {\n const resolved = resolveOptions(optionsOrPreset);\n\n return (input: string): string => applySanitization(input, resolved);\n};\n\n/**\n * Sanitizes Arabic text according to a preset or custom options.\n *\n * Presets:\n * - `'light'`: NFC, zero-width removal, collapse/trim spaces.\n * - `'search'`: removes diacritics and tatweel, normalizes Alif and ูโู, removes Hijri marker.\n * - `'aggressive'`: ideal for FTS; keeps letters+spaces only and strips common noise.\n *\n * Custom options:\n * - Passing an options object overlays the selected `base` preset (default `'light'`).\n * - Use `base: 'none'` to apply **only** the rules you specify (e.g., tatweel only).\n *\n * **Batch processing**: Pass an array of strings for optimized batch processing.\n * Options are resolved once and applied to all strings, providing significant\n * performance gains over calling the function in a loop.\n *\n * Examples:\n * ```ts\n * sanitizeArabic('ุฃุจูููุชููููููุฉู', { base: 'none', stripTatweel: true }); // 'ุฃุจุชูููุฉู'\n * sanitizeArabic('1435/3/29 ูู', 'aggressive'); // '1435 3 29'\n * sanitizeArabic('ุงููุณููููุงู
ู ุนูููููููู
ู', 'search'); // 'ุงูุณูุงู
ุนูููู
'\n *\n * // Batch processing (optimized):\n * sanitizeArabic(['text1', 'text2', 'text3'], 'search'); // ['result1', 'result2', 'result3']\n * ```\n */\nexport function sanitizeArabic(input: string, optionsOrPreset?: SanitizePreset | SanitizeOptions): string;\nexport function sanitizeArabic(input: string[], optionsOrPreset?: SanitizePreset | SanitizeOptions): string[];\nexport function sanitizeArabic(\n input: string | string[],\n optionsOrPreset: SanitizePreset | SanitizeOptions = 'search',\n): string | string[] {\n // Handle array input with optimized batch processing\n if (Array.isArray(input)) {\n if (input.length === 0) {\n return [];\n }\n\n const resolved = resolveOptions(optionsOrPreset);\n\n // Per-string processing using the optimized single-pass sanitizer\n const results: string[] = new Array(input.length);\n\n for (let i = 0; i < input.length; i++) {\n results[i] = applySanitization(input[i], resolved);\n }\n\n return results;\n }\n\n // Single string: resolve options and apply\n if (!input) {\n return '';\n }\n\n const resolved = resolveOptions(optionsOrPreset);\n\n return applySanitization(input, resolved);\n}\n","/**\n * Calculates Levenshtein distance between two strings using space-optimized dynamic programming.\n * The Levenshtein distance is the minimum number of single-character edits (insertions,\n * deletions, or substitutions) required to change one string into another.\n *\n * @param textA - First string to compare\n * @param textB - Second string to compare\n * @returns Minimum edit distance between the two strings\n * @complexity Time: O(m*n), Space: O(min(m,n)) where m,n are string lengths\n * @example\n * calculateLevenshteinDistance('kitten', 'sitting') // Returns 3\n * calculateLevenshteinDistance('', 'hello') // Returns 5\n */\nexport const calculateLevenshteinDistance = (textA: string, textB: string): number => {\n const lengthA = textA.length;\n const lengthB = textB.length;\n\n if (lengthA === 0) {\n return lengthB;\n }\n\n if (lengthB === 0) {\n return lengthA;\n }\n\n // Use shorter string for the array to optimize space\n const [shorter, longer] = lengthA <= lengthB ? [textA, textB] : [textB, textA];\n const shortLen = shorter.length;\n const longLen = longer.length;\n\n let previousRow = Array.from({ length: shortLen + 1 }, (_, index) => index);\n\n for (let i = 1; i <= longLen; i++) {\n const currentRow = [i];\n\n for (let j = 1; j <= shortLen; j++) {\n const substitutionCost = longer[i - 1] === shorter[j - 1] ? 0 : 1;\n const minCost = Math.min(\n previousRow[j] + 1, // deletion\n currentRow[j - 1] + 1, // insertion\n previousRow[j - 1] + substitutionCost, // substitution\n );\n currentRow.push(minCost);\n }\n\n previousRow = currentRow;\n }\n\n return previousRow[shortLen];\n};\n\n/**\n * Early exit check for bounded Levenshtein distance.\n */\nconst shouldEarlyExit = (a: string, b: string, maxDist: number): number | null => {\n if (Math.abs(a.length - b.length) > maxDist) {\n return maxDist + 1;\n }\n if (a.length === 0) {\n return b.length <= maxDist ? b.length : maxDist + 1;\n }\n if (b.length === 0) {\n return a.length <= maxDist ? a.length : maxDist + 1;\n }\n return null;\n};\n\n/**\n * Initializes arrays for bounded Levenshtein calculation.\n */\nconst initializeBoundedArrays = (m: number): [Int16Array, Int16Array] => {\n const prev = new Int16Array(m + 1);\n const curr = new Int16Array(m + 1);\n for (let j = 0; j <= m; j++) {\n prev[j] = j;\n }\n return [prev, curr];\n};\n\n/**\n * Calculates the bounds for the current row in bounded Levenshtein.\n */\nconst getRowBounds = (i: number, maxDist: number, m: number) => ({\n from: Math.max(1, i - maxDist),\n to: Math.min(m, i + maxDist),\n});\n\n/**\n * Processes a single cell in the bounded Levenshtein matrix.\n */\nconst processBoundedCell = (a: string, b: string, i: number, j: number, prev: Int16Array, curr: Int16Array): number => {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n const del = prev[j] + 1;\n const ins = curr[j - 1] + 1;\n const sub = prev[j - 1] + cost;\n return Math.min(del, ins, sub);\n};\n\n/**\n * Processes a single row in bounded Levenshtein calculation.\n */\nconst processBoundedRow = (\n a: string,\n b: string,\n i: number,\n maxDist: number,\n prev: Int16Array,\n curr: Int16Array,\n): number => {\n const m = b.length;\n const big = maxDist + 1;\n const { from, to } = getRowBounds(i, maxDist, m);\n\n curr[0] = i;\n let rowMin = i;\n\n // Fill out-of-bounds cells\n for (let j = 1; j < from; j++) {\n curr[j] = big;\n }\n for (let j = to + 1; j <= m; j++) {\n curr[j] = big;\n }\n\n // Process valid range\n for (let j = from; j <= to; j++) {\n const val = processBoundedCell(a, b, i, j, prev, curr);\n curr[j] = val;\n if (val < rowMin) {\n rowMin = val;\n }\n }\n\n return rowMin;\n};\n\n/**\n * Calculates bounded Levenshtein distance with early termination.\n * More efficient when you only care about distances up to a threshold.\n */\nexport const boundedLevenshtein = (a: string, b: string, maxDist: number): number => {\n const big = maxDist + 1;\n\n // Early exit checks\n const earlyResult = shouldEarlyExit(a, b, maxDist);\n if (earlyResult !== null) {\n return earlyResult;\n }\n\n // Ensure a is shorter for optimization\n if (a.length > b.length) {\n return boundedLevenshtein(b, a, maxDist);\n }\n\n // use `let` so we can swap references instead of copying contents\n let [prev, curr] = initializeBoundedArrays(b.length);\n\n for (let i = 1; i <= a.length; i++) {\n const rowMin = processBoundedRow(a, b, i, maxDist, prev, curr);\n if (rowMin > maxDist) {\n return big;\n }\n\n // O(1) swap instead of O(m) copy\n const tmp = prev;\n prev = curr;\n curr = tmp;\n }\n\n return prev[b.length] <= maxDist ? prev[b.length] : big;\n};\n","import { calculateLevenshteinDistance } from './levenshthein';\nimport { sanitizeArabic } from './sanitize';\n\n// Alignment scoring constants\nconst ALIGNMENT_SCORES = {\n GAP_PENALTY: -1,\n MISMATCH_PENALTY: -2,\n PERFECT_MATCH: 2,\n SOFT_MATCH: 1,\n} as const;\n\n/**\n * Calculates similarity ratio between two strings as a value between 0.0 and 1.0.\n * Uses Levenshtein distance normalized by the length of the longer string.\n * A ratio of 1.0 indicates identical strings, 0.0 indicates completely different strings.\n *\n * @param textA - First string to compare\n * @param textB - Second string to compare\n * @returns Similarity ratio from 0.0 (completely different) to 1.0 (identical)\n * @example\n * calculateSimilarity('hello', 'hello') // Returns 1.0\n * calculateSimilarity('hello', 'help') // Returns 0.6\n */\nexport const calculateSimilarity = (textA: string, textB: string): number => {\n const maxLength = Math.max(textA.length, textB.length) || 1;\n const distance = calculateLevenshteinDistance(textA, textB);\n return (maxLength - distance) / maxLength;\n};\n\n/**\n * Checks if two texts are similar after Arabic normalization.\n * Normalizes both texts by removing diacritics and decorative elements,\n * then compares their similarity against the provided threshold.\n *\n * @param textA - First text to compare\n * @param textB - Second text to compare\n * @param threshold - Similarity threshold (0.0 to 1.0)\n * @returns True if normalized texts meet the similarity threshold\n * @example\n * areSimilarAfterNormalization('ุงูุณูููุงู
', 'ุงูุณูุงู
', 0.9) // Returns true\n */\nexport const areSimilarAfterNormalization = (textA: string, textB: string, threshold: number = 0.6): boolean => {\n const normalizedA = sanitizeArabic(textA);\n const normalizedB = sanitizeArabic(textB);\n return calculateSimilarity(normalizedA, normalizedB) >= threshold;\n};\n\n/**\n * Calculates alignment score for two tokens in sequence alignment.\n * Uses different scoring criteria: perfect match after normalization gets highest score,\n * typo symbols or highly similar tokens get soft match score, mismatches get penalty.\n *\n * @param tokenA - First token to score\n * @param tokenB - Second token to score\n * @param typoSymbols - Array of special symbols that get preferential treatment\n * @param similarityThreshold - Threshold for considering tokens highly similar\n * @returns Alignment score (higher is better match)\n * @example\n * calculateAlignmentScore('hello', 'hello', [], 0.8) // Returns 2 (perfect match)\n * calculateAlignmentScore('hello', 'help', [], 0.8) // Returns 1 or -2 based on similarity\n */\nexport const calculateAlignmentScore = (\n tokenA: string,\n tokenB: string,\n typoSymbols: string[],\n similarityThreshold: number,\n): number => {\n const normalizedA = sanitizeArabic(tokenA);\n const normalizedB = sanitizeArabic(tokenB);\n\n if (normalizedA === normalizedB) {\n return ALIGNMENT_SCORES.PERFECT_MATCH;\n }\n\n const isTypoSymbol = typoSymbols.includes(tokenA) || typoSymbols.includes(tokenB);\n const isHighlySimilar = calculateSimilarity(normalizedA, normalizedB) >= similarityThreshold;\n\n return isTypoSymbol || isHighlySimilar ? ALIGNMENT_SCORES.SOFT_MATCH : ALIGNMENT_SCORES.MISMATCH_PENALTY;\n};\n\ntype AlignedTokenPair = [null | string, null | string];\n\ntype AlignmentCell = {\n direction: 'diagonal' | 'left' | 'up' | null;\n score: number;\n};\n\n/**\n * Backtracks through the scoring matrix to reconstruct optimal sequence alignment.\n * Follows the directional indicators in the matrix to build the sequence of aligned\n * token pairs from the Needleman-Wunsch algorithm.\n *\n * @param matrix - Scoring matrix with directional information from alignment\n * @param tokensA - First sequence of tokens\n * @param tokensB - Second sequence of tokens\n * @returns Array of aligned token pairs, where null indicates a gap\n * @throws Error if invalid alignment direction is encountered\n */\nexport const backtrackAlignment = (\n matrix: AlignmentCell[][],\n tokensA: string[],\n tokensB: string[],\n): AlignedTokenPair[] => {\n const alignment: AlignedTokenPair[] = [];\n let i = tokensA.length;\n let j = tokensB.length;\n\n while (i > 0 || j > 0) {\n const currentCell = matrix[i][j];\n\n switch (currentCell.direction) {\n case 'diagonal':\n alignment.push([tokensA[--i], tokensB[--j]]);\n break;\n case 'left':\n alignment.push([null, tokensB[--j]]);\n break;\n case 'up':\n alignment.push([tokensA[--i], null]);\n break;\n default:\n throw new Error('Invalid alignment direction');\n }\n }\n\n return alignment.reverse();\n};\n\n/**\n * Initializes the scoring matrix with gap penalties.\n *\n * @param lengthA - Length of the first token sequence.\n * @param lengthB - Length of the second token sequence.\n * @returns A matrix seeded with gap penalties for alignment.\n */\nconst initializeScoringMatrix = (lengthA: number, lengthB: number): AlignmentCell[][] => {\n const matrix: AlignmentCell[][] = Array.from({ length: lengthA + 1 }, () =>\n Array.from({ length: lengthB + 1 }, () => ({ direction: null, score: 0 })),\n );\n\n // Initialize first row and column with gap penalties\n for (let i = 1; i <= lengthA; i++) {\n matrix[i][0] = { direction: 'up', score: i * ALIGNMENT_SCORES.GAP_PENALTY };\n }\n for (let j = 1; j <= lengthB; j++) {\n matrix[0][j] = { direction: 'left', score: j * ALIGNMENT_SCORES.GAP_PENALTY };\n }\n\n return matrix;\n};\n\n/**\n * Determines the best alignment direction and score for a cell.\n *\n * @param diagonalScore - Score achieved by aligning tokens diagonally.\n * @param upScore - Score achieved by inserting a gap in the second sequence.\n * @param leftScore - Score achieved by inserting a gap in the first sequence.\n * @returns The direction and score that maximize the alignment.\n */\nconst getBestAlignment = (\n diagonalScore: number,\n upScore: number,\n leftScore: number,\n): { direction: 'diagonal' | 'up' | 'left'; score: number } => {\n const maxScore = Math.max(diagonalScore, upScore, leftScore);\n\n if (maxScore === diagonalScore) {\n return { direction: 'diagonal', score: maxScore };\n }\n if (maxScore === upScore) {\n return { direction: 'up', score: maxScore };\n }\n return { direction: 'left', score: maxScore };\n};\n\n/**\n * Performs global sequence alignment using the Needleman-Wunsch algorithm.\n * Aligns two token sequences to find the optimal pairing that maximizes\n * the total alignment score, handling insertions, deletions, and substitutions.\n *\n * @param tokensA - First sequence of tokens to align\n * @param tokensB - Second sequence of tokens to align\n * @param typoSymbols - Special symbols that affect scoring\n * @param similarityThreshold - Threshold for high similarity scoring\n * @returns Array of aligned token pairs, with null indicating gaps\n * @example\n * alignTokenSequences(['a', 'b'], ['a', 'c'], [], 0.8)\n * // Returns [['a', 'a'], ['b', 'c']]\n */\nexport const alignTokenSequences = (\n tokensA: string[],\n tokensB: string[],\n typoSymbols: string[],\n similarityThreshold: number,\n): AlignedTokenPair[] => {\n const lengthA = tokensA.length;\n const lengthB = tokensB.length;\n\n const matrix = initializeScoringMatrix(lengthA, lengthB);\n const typoSymbolsSet = new Set(typoSymbols);\n const normalizedA = tokensA.map((t) => sanitizeArabic(t));\n const normalizedB = tokensB.map((t) => sanitizeArabic(t));\n\n // Fill scoring matrix\n for (let i = 1; i <= lengthA; i++) {\n for (let j = 1; j <= lengthB; j++) {\n const aNorm = normalizedA[i - 1];\n const bNorm = normalizedB[j - 1];\n let alignmentScore: number;\n if (aNorm === bNorm) {\n alignmentScore = ALIGNMENT_SCORES.PERFECT_MATCH;\n } else {\n const isTypo = typoSymbolsSet.has(tokensA[i - 1]) || typoSymbolsSet.has(tokensB[j - 1]);\n const highSim = calculateSimilarity(aNorm, bNorm) >= similarityThreshold;\n alignmentScore = isTypo || highSim ? ALIGNMENT_SCORES.SOFT_MATCH : ALIGNMENT_SCORES.MISMATCH_PENALTY;\n }\n\n const diagonalScore = matrix[i - 1][j - 1].score + alignmentScore;\n const upScore = matrix[i - 1][j].score + ALIGNMENT_SCORES.GAP_PENALTY;\n const leftScore = matrix[i][j - 1].score + ALIGNMENT_SCORES.GAP_PENALTY;\n\n const { direction, score } = getBestAlignment(diagonalScore, upScore, leftScore);\n matrix[i][j] = { direction, score };\n }\n }\n\n return backtrackAlignment(matrix, tokensA, tokensB);\n};\n","import { sanitizeArabic } from './utils/sanitize';\nimport { areSimilarAfterNormalization, calculateSimilarity } from './utils/similarity';\n\n/**\n * Aligns split text segments to match target lines by finding the best order.\n *\n * This function handles cases where text lines have been split into segments\n * and need to be merged back together in the correct order. It compares\n * different arrangements of the segments against target lines to find the\n * best match based on similarity scores.\n *\n * @param targetLines - Array where each element is either a string to align against, or falsy to skip alignment\n * @param segmentLines - Array of text segments that may represent split versions of target lines.\n * @returns Array of aligned text lines\n */\nexport const alignTextSegments = (targetLines: string[], segmentLines: string[]) => {\n const alignedLines: string[] = [];\n let segmentIndex = 0;\n\n for (const targetLine of targetLines) {\n if (segmentIndex >= segmentLines.length) {\n break;\n }\n\n if (targetLine) {\n // Process line that needs alignment\n const { result, segmentsConsumed } = processAlignmentTarget(targetLine, segmentLines, segmentIndex);\n\n if (result) {\n alignedLines.push(result);\n }\n segmentIndex += segmentsConsumed;\n } else {\n // For lines that don't need alignment, use one-to-one correspondence\n alignedLines.push(segmentLines[segmentIndex]);\n segmentIndex++;\n }\n }\n\n // Add any remaining segments that were not processed\n if (segmentIndex < segmentLines.length) {\n alignedLines.push(...segmentLines.slice(segmentIndex));\n }\n\n return alignedLines;\n};\n\n/**\n * Tries to merge two candidate segments in both possible orders and returns the best match.\n *\n * @param targetLine - The line we are trying to reconstruct.\n * @param partA - The first candidate segment to evaluate.\n * @param partB - The second candidate segment to evaluate.\n * @returns The merged segment that best matches the target line after normalization.\n */\nconst findBestSegmentMerge = (targetLine: string, partA: string, partB: string) => {\n const mergedForward = `${partA} ${partB}`;\n const mergedReversed = `${partB} ${partA}`;\n\n const normalizedTarget = sanitizeArabic(targetLine);\n const scoreForward = calculateSimilarity(normalizedTarget, sanitizeArabic(mergedForward));\n const scoreReversed = calculateSimilarity(normalizedTarget, sanitizeArabic(mergedReversed));\n\n return scoreForward >= scoreReversed ? mergedForward : mergedReversed;\n};\n\n/**\n * Processes a single target line that needs alignment.\n *\n * @param targetLine - The line we are attempting to align to.\n * @param segmentLines - The collection of available text segments.\n * @param segmentIndex - The current index within {@link segmentLines} to consider.\n * @returns An object containing the resulting aligned text and how many segments were consumed.\n */\nconst processAlignmentTarget = (targetLine: string, segmentLines: string[], segmentIndex: number) => {\n const currentSegment = segmentLines[segmentIndex];\n\n // First, check if the current segment is already a good match\n if (areSimilarAfterNormalization(targetLine, currentSegment)) {\n return { result: currentSegment, segmentsConsumed: 1 };\n }\n\n // If not a direct match, try to merge two segments\n const partA = segmentLines[segmentIndex];\n const partB = segmentLines[segmentIndex + 1];\n\n // Ensure we have two parts to merge\n if (!partA || !partB) {\n return partA ? { result: partA, segmentsConsumed: 1 } : { result: '', segmentsConsumed: 0 };\n }\n\n const bestMerge = findBestSegmentMerge(targetLine, partA, partB);\n return { result: bestMerge, segmentsConsumed: 2 };\n};\n","/**\n * Represents an error found when checking balance of quotes or brackets in text.\n */\ntype BalanceError = {\n /** The character that caused the error */\n char: string;\n /** The position of the character in the string */\n index: number;\n /** The reason for the error */\n reason: 'mismatched' | 'unclosed' | 'unmatched';\n /** The type of character that caused the error */\n type: 'bracket' | 'quote';\n};\n\n/**\n * Result of a balance check operation.\n */\ntype BalanceResult = {\n /** Array of errors found during balance checking */\n errors: BalanceError[];\n /** Whether the text is properly balanced */\n isBalanced: boolean;\n};\n\n/**\n * Checks if all double quotes in a string are balanced and returns detailed error information.\n *\n * A string has balanced quotes when every opening quote has a corresponding closing quote.\n * This function counts all quote characters and determines if there's an even number of them.\n * If there's an odd number, the last quote is marked as unmatched.\n *\n * @param str - The string to check for quote balance\n * @returns An object containing balance status and any errors found\n *\n * @example\n * ```typescript\n * checkQuoteBalance('Hello \"world\"') // { errors: [], isBalanced: true }\n * checkQuoteBalance('Hello \"world') // { errors: [{ char: '\"', index: 6, reason: 'unmatched', type: 'quote' }], isBalanced: false }\n * ```\n */\nconst checkQuoteBalance = (str: string): BalanceResult => {\n const errors: BalanceError[] = [];\n let quoteCount = 0;\n let lastQuoteIndex = -1;\n\n for (let i = 0; i < str.length; i++) {\n if (str[i] === '\"') {\n quoteCount++;\n lastQuoteIndex = i;\n }\n }\n\n const isBalanced = quoteCount % 2 === 0;\n\n if (!isBalanced && lastQuoteIndex !== -1) {\n errors.push({\n char: '\"',\n index: lastQuoteIndex,\n reason: 'unmatched',\n type: 'quote',\n });\n }\n\n return { errors, isBalanced };\n};\n\n/** Mapping of opening brackets to their corresponding closing brackets */\nexport const BRACKETS = { 'ยซ': 'ยป', '(': ')', '[': ']', '{': '}' };\n\n/** Set of all opening bracket characters */\nexport const OPEN_BRACKETS = new Set(['ยซ', '(', '[', '{']);\n\n/** Set of all closing bracket characters */\nexport const CLOSE_BRACKETS = new Set(['ยป', ')', ']', '}']);\n\n/**\n * Checks if all brackets in a string are properly balanced and returns detailed error information.\n *\n * A string has balanced brackets when:\n * - Every opening bracket has a corresponding closing bracket\n * - Brackets are properly nested (no crossing pairs)\n * - Each closing bracket matches the most recent unmatched opening bracket\n *\n * Supports the following bracket pairs: (), [], {}, ยซยป\n *\n * @param str - The string to check for bracket balance\n * @returns An object containing balance status and any errors found\n *\n * @example\n * ```typescript\n * checkBracketBalance('(hello [world])') // { errors: [], isBalanced: true }\n * checkBracketBalance('(hello [world)') // { errors: [{ char: '[', index: 7, reason: 'unclosed', type: 'bracket' }], isBalanced: false }\n * checkBracketBalance('(hello ]world[') // { errors: [...], isBalanced: false }\n * ```\n */\nconst checkBracketBalance = (str: string): BalanceResult => {\n const errors: BalanceError[] = [];\n const stack: Array<{ char: string; index: number }> = [];\n\n for (let i = 0; i < str.length; i++) {\n const char = str[i];\n\n if (OPEN_BRACKETS.has(char)) {\n stack.push({ char, index: i });\n } else if (CLOSE_BRACKETS.has(char)) {\n const lastOpen = stack.pop();\n\n if (!lastOpen) {\n errors.push({\n char,\n index: i,\n reason: 'unmatched',\n type: 'bracket',\n });\n } else if (BRACKETS[lastOpen.char as keyof typeof BRACKETS] !== char) {\n errors.push({\n char: lastOpen.char,\n index: lastOpen.index,\n reason: 'mismatched',\n type: 'bracket',\n });\n errors.push({\n char,\n index: i,\n reason: 'mismatched',\n type: 'bracket',\n });\n }\n }\n }\n\n stack.forEach(({ char, index }) => {\n errors.push({\n char,\n index,\n reason: 'unclosed',\n type: 'bracket',\n });\n });\n\n return { errors, isBalanced: errors.length === 0 };\n};\n\n/**\n * Checks if both quotes and brackets are balanced in a string and returns detailed error information.\n *\n * This function combines the results of both quote and bracket balance checking,\n * providing a comprehensive analysis of all balance issues in the text.\n * The errors are sorted by their position in the string for easier debugging.\n *\n * @param str - The string to check for overall balance\n * @returns An object containing combined balance status and all errors found, sorted by position\n *\n * @example\n * ```typescript\n * checkBalance('Hello \"world\" and (test)') // { errors: [], isBalanced: true }\n * checkBalance('Hello \"world and (test') // { errors: [...], isBalanced: false }\n * ```\n */\nexport const checkBalance = (str: string): BalanceResult => {\n const quoteResult = checkQuoteBalance(str);\n const bracketResult = checkBracketBalance(str);\n\n return {\n errors: [...quoteResult.errors, ...bracketResult.errors].sort((a, b) => a.index - b.index),\n isBalanced: quoteResult.isBalanced && bracketResult.isBalanced,\n };\n};\n\n/**\n * Enhanced error detection that returns absolute character positions for use with HighlightableTextarea.\n *\n * This interface extends the basic BalanceError to include absolute positioning\n * across multiple lines of text, making it suitable for text editors and\n * syntax highlighters that need precise character positioning.\n */\nexport interface CharacterError {\n /** Absolute character position from the start of the entire text */\n absoluteIndex: number;\n /** The character that caused the error */\n char: string;\n /** The reason for the error */\n reason: 'mismatched' | 'unclosed' | 'unmatched';\n /** The type of character that caused the error */\n type: 'bracket' | 'quote';\n}\n\n/**\n * Gets detailed character-level errors for unbalanced quotes and brackets in multi-line text.\n *\n * This function processes text line by line, but only checks lines longer than 10 characters\n * for balance issues. It returns absolute positions that can be used with text editors\n * or highlighting components that need precise character positioning across the entire text.\n *\n * The absolute index accounts for newline characters between lines, providing accurate\n * positioning for the original text string.\n *\n * @param text - The multi-line text to analyze for balance errors\n * @returns Array of character errors with absolute positioning information\n *\n * @example\n * ```typescript\n * const text = 'Line 1 with \"quote\\nLine 2 with (bracket';\n * const errors = getUnbalancedErrors(text);\n * // Returns errors with absoluteIndex pointing to exact character positions\n * ```\n */\nexport const getUnbalancedErrors = (text: string): CharacterError[] => {\n const characterErrors: CharacterError[] = [];\n const lines = text.split('\\n');\n let absoluteIndex = 0;\n\n lines.forEach((line, lineIndex) => {\n if (line.length > 10) {\n const balanceResult = checkBalance(line);\n if (!balanceResult.isBalanced) {\n balanceResult.errors.forEach((error) => {\n characterErrors.push({\n absoluteIndex: absoluteIndex + error.index,\n char: error.char,\n reason: error.reason,\n type: error.type,\n });\n });\n }\n }\n // Add 1 for the newline character (except for the last line)\n absoluteIndex += line.length + (lineIndex < lines.length - 1 ? 1 : 0);\n });\n\n return characterErrors;\n};\n\n/**\n * Checks if all double quotes in a string are balanced.\n *\n * This is a convenience function that returns only the boolean result\n * without detailed error information.\n *\n * @param str - The string to check for quote balance\n * @returns True if quotes are balanced, false otherwise\n *\n * @example\n * ```typescript\n * areQuotesBalanced('Hello \"world\"') // true\n * areQuotesBalanced('Hello \"world') // false\n * ```\n */\nexport const areQuotesBalanced = (str: string): boolean => {\n return checkQuoteBalance(str).isBalanced;\n};\n\n/**\n * Checks if all brackets in a string are properly balanced.\n *\n * This is a convenience function that returns only the boolean result\n * without detailed error information.\n *\n * @param str - The string to check for bracket balance\n * @returns True if brackets are balanced, false otherwise\n *\n * @example\n * ```typescript\n * areBracketsBalanced('(hello [world])') // true\n * areBracketsBalanced('(hello [world') // false\n * ```\n */\nexport const areBracketsBalanced = (str: string): boolean => {\n return checkBracketBalance(str).isBalanced;\n};\n\n/**\n * Checks if both quotes and brackets are balanced in a string.\n *\n * This is a convenience function that returns only the boolean result\n * without detailed error information.\n *\n * @param str - The string to check for overall balance\n * @returns True if both quotes and brackets are balanced, false otherwise\n *\n * @example\n * ```typescript\n * isBalanced('Hello \"world\" and (test)') // true\n * isBalanced('Hello \"world and (test') // false\n * ```\n */\nexport const isBalanced = (str: string): boolean => {\n return checkBalance(str).isBalanced;\n};\n","export const INTAHA_ACTUAL = 'ุงูู';\n\n/**\n * Collection of regex patterns used throughout the library for text processing\n */\nexport const PATTERNS = {\n /** Matches Arabic characters across all Unicode blocks */\n arabicCharacters: /[\\u0600-\\u06FF\\u0750-\\u077F\\u08A0-\\u08FF\\uFB50-\\uFDFF\\uFE70-\\uFEFF]/,\n\n /** Matches Arabic-Indic digits (ู -ูฉ) and Western digits (0-9) */\n arabicDigits: /[0-9\\u0660-\\u0669]+/,\n\n /** Matches footnote references at the start of a line with Arabic-Indic digits: ^\\([\\u0660-\\u0669]+\\) */\n arabicFootnoteReferenceRegex: /^\\([\\u0660-\\u0669]+\\)/g,\n\n /** Matches Arabic letters and digits (both Western 0-9 and Arabic-Indic ู -ูฉ) */\n arabicLettersAndDigits: /[0-9\\u0621-\\u063A\\u0641-\\u064A\\u0660-\\u0669]+/g,\n\n /** Matches Arabic punctuation marks and whitespace characters */\n arabicPunctuationAndWhitespace: /[\\s\\u060C\\u061B\\u061F\\u06D4]+/,\n\n /** Matches footnote references with Arabic-Indic digits in parentheses: \\([\\u0660-\\u0669]+\\) */\n arabicReferenceRegex: /\\([\\u0660-\\u0669]+\\)/g,\n\n /** Matches embedded footnotes within text: \\([0-9\\u0660-\\u0669]+\\) */\n footnoteEmbedded: /\\([0-9\\u0660-\\u0669]+\\)/,\n\n /** Matches standalone footnote markers at line start/end: ^\\(?[0-9\\u0660-\\u0669]+\\)?[ุ.]?$ */\n footnoteStandalone: /^\\(?[0-9\\u0660-\\u0669]+\\)?[ุ.]?$/,\n\n /** Matches invalid/problematic footnote references: empty \"()\" or OCR-confused endings */\n invalidReferenceRegex: /\\(\\)|\\([.1OV9]+\\)/g, // Combined pattern for detecting any invalid/problematic references\n\n /** Matches OCR-confused footnote references at line start with characters like .1OV9 */\n ocrConfusedFootnoteReferenceRegex: /^\\([.1OV9]+\\)/g,\n\n /** Matches OCR-confused footnote references with characters commonly misread as Arabic digits */\n ocrConfusedReferenceRegex: /\\([.1OV9]+\\)/g,\n\n /** Matches one or more whitespace characters */\n whitespace: /\\s+/,\n};\n\n/**\n * Extracts the first sequence of Arabic or Western digits from text.\n * Used primarily for footnote number comparison to match related footnote elements.\n *\n * @param text - Text containing digits to extract\n * @returns First digit sequence found, or empty string if none found\n * @example\n * extractDigits('(ูฅ)ุฃุฎุฑุฌู ุงูุจุฎุงุฑู') // Returns 'ูฅ'\n * extractDigits('See note (123)') // Returns '123'\n */\nexport const extractDigits = (text: string): string => {\n const match = text.match(PATTERNS.arabicDigits);\n return match ? match[0] : '';\n};\n\n/**\n * Tokenizes text into individual words while preserving special symbols.\n * Adds spacing around preserved symbols to ensure they are tokenized separately,\n * then splits on whitespace.\n *\n * @param text - Text to tokenize\n * @param preserveSymbols - Array of symbols that should be tokenized as separate tokens\n * @returns Array of tokens, or empty array if input is empty/whitespace\n * @example\n * tokenizeText('Hello ๏ทบ world', ['๏ทบ']) // Returns ['Hello', '๏ทบ', 'world']\n */\nexport const tokenizeText = (text: string, preserveSymbols: string[] = []): string[] => {\n let processedText = text;\n\n // Add spaces around each preserve symbol to ensure they're tokenized separately\n for (const symbol of preserveSymbols) {\n const symbolRegex = new RegExp(symbol, 'g');\n processedText = processedText.replace(symbolRegex, ` ${symbol} `);\n }\n\n return processedText.trim().split(PATTERNS.whitespace).filter(Boolean);\n};\n\n/**\n * Handles fusion of standalone and embedded footnotes during token processing.\n * Detects patterns where standalone footnotes should be merged with embedded ones\n * or where trailing standalone footnotes should be skipped.\n *\n * @param result - Current result array being built\n * @param previousToken - The previous token in the sequence\n * @param currentToken - The current token being processed\n * @returns True if the current token was handled (fused or skipped), false otherwise\n * @example\n * // (ูฅ) + (ูฅ)ุฃุฎุฑุฌู โ result gets (ูฅ)ุฃุฎุฑุฌู\n * // (ูฅ)ุฃุฎุฑุฌู + (ูฅ) โ (ูฅ) is skipped\n */\nexport const handleFootnoteFusion = (result: string[], previousToken: string, currentToken: string): boolean => {\n const prevIsStandalone = PATTERNS.footnoteStandalone.test(previousToken);\n const currHasEmbedded = PATTERNS.footnoteEmbedded.test(currentToken);\n const currIsStandalone = PATTERNS.footnoteStandalone.test(currentToken);\n const prevHasEmbedded = PATTERNS.footnoteEmbedded.test(previousToken);\n\n const prevDigits = extractDigits(previousToken);\n const currDigits = extractDigits(currentToken);\n\n // Replace standalone with fused version: (ูฅ) + (ูฅ)ุฃุฎุฑุฌู โ (ูฅ)ุฃุฎุฑุฌู\n if (prevIsStandalone && currHasEmbedded && prevDigits === currDigits) {\n result[result.length - 1] = currentToken;\n return true;\n }\n\n // Skip trailing standalone: (ูฅ)ุฃุฎุฑุฌู + (ูฅ) โ (ูฅ)ุฃุฎุฑุฌู\n if (prevHasEmbedded && currIsStandalone && prevDigits === currDigits) {\n return true;\n }\n\n return false;\n};\n\n/**\n * Handles selection logic for tokens with embedded footnotes during alignment.\n * Prefers tokens that contain embedded footnotes over plain text, and among\n * tokens with embedded footnotes, prefers the shorter one.\n *\n * @param tokenA - First token to compare\n * @param tokenB - Second token to compare\n * @returns Array containing selected token(s), or null if no special handling needed\n * @example\n * handleFootnoteSelection('text', '(ูก)text') // Returns ['(ูก)text']\n * handleFootnoteSelection('(ูก)longtext', '(ูก)text') // Returns ['(ูก)text']\n */\nexport const handleFootnoteSelection = (tokenA: string, tokenB: string): null | string[] => {\n const aHasEmbedded = PATTERNS.footnoteEmbedded.test(tokenA);\n const bHasEmbedded = PATTERNS.footnoteEmbedded.test(tokenB);\n\n if (aHasEmbedded && !bHasEmbedded) {\n return [tokenA];\n }\n if (bHasEmbedded && !aHasEmbedded) {\n return [tokenB];\n }\n if (aHasEmbedded && bHasEmbedded) {\n return [tokenA.length <= tokenB.length ? tokenA : tokenB];\n }\n\n return null;\n};\n\n/**\n * Handles selection logic for standalone footnote tokens during alignment.\n * Manages cases where one or both tokens are standalone footnotes, preserving\n * both tokens when one is a footnote and the other is regular text.\n *\n * @param tokenA - First token to compare\n * @param tokenB - Second token to compare\n * @returns Array containing selected token(s), or null if no special handling needed\n * @example\n * handleStandaloneFootnotes('(ูก)', 'text') // Returns ['(ูก)', 'text']\n * handleStandaloneFootnotes('(ูก)', '(ูข)') // Returns ['(ูก)'] (shorter one)\n */\nexport const handleStandaloneFootnotes = (tokenA: string, tokenB: string): null | string[] => {\n const aIsFootnote = PATTERNS.footnoteStandalone.test(tokenA);\n const bIsFootnote = PATTERNS.footnoteStandalone.test(tokenB);\n\n if (aIsFootnote && !bIsFootnote) {\n return [tokenA, tokenB];\n }\n if (bIsFootnote && !aIsFootnote) {\n return [tokenB, tokenA];\n }\n if (aIsFootnote && bIsFootnote) {\n return [tokenA.length <= tokenB.length ? tokenA : tokenB];\n }\n\n return null;\n};\n\n/**\n * Removes simple footnote references from Arabic text.\n * Handles footnotes in the format (ยฌ[Arabic numerals]) where ยฌ is the not symbol (U+00AC).\n *\n * @param text - The input text containing footnote references to remove\n * @returns The text with footnote references removed and extra spaces normalized\n *\n * @example\n * ```typescript\n * removeFootnoteReferencesSimple(\"ูุฐุง ุงููุต (ยฌูกูขูฃ) ูุญุชูู ุนูู ุญุงุดูุฉ\")\n * // Returns: \"ูุฐุง ุงููุต ูุญุชูู ุนูู ุญุงุดูุฉ\"\n * ```\n */\nexport const removeFootnoteReferencesSimple = (text: string): string => {\n return text\n .replace(/ ?\\(\\u00AC[\\u0660-\\u0669]+\\) ?/g, ' ')\n .replace(/ +/g, ' ')\n .trim();\n};\n\n/**\n * Removes single digit footnote references and extended footnote formats from Arabic text.\n * Handles footnotes in the format:\n * - ([single Arabic digit]) - e.g., (ูฃ)\n * - ([single Arabic digit] [single Arabic letter]) - e.g., (ูฃ ู
), (ูฅ ู), (ูง ุจ)\n *\n * @param text - The input text containing footnote references to remove\n * @returns The text with footnote references removed and extra spaces normalized\n *\n * @example\n * ```typescript\n * removeSingleDigitFootnoteReferences(\"ูุฐุง ุงููุต (ูฃ) ูุงูุขุฎุฑ (ูฅ ู
) ูุงูุซุงูุซ (ูง ู) ูุญุชูู ุนูู ุญูุงุดู\")\n * // Returns: \"ูุฐุง ุงููุต ูุงูุขุฎุฑ ูุงูุซุงูุซ ูุญุชูู ุนูู ุญูุงุดู\"\n * ```\n */\nexport const removeSingleDigitFootnoteReferences = (text: string): string => {\n // Remove single digit footnotes with optional Arabic letter suffix: (ูฃ) or (ูฃ ู
) or (ูฅ ู) etc.\n return text\n .replace(/ ?\\([ู -ูฉ]{1}(\\s+[\\u0600-\\u06FF])?\\) ?/g, ' ')\n .replace(/ +/g, ' ')\n .trim();\n};\n\n/**\n * Standardizes standalone Hijri symbol ู to ูู when following Arabic digits\n * @param text - Input text to process\n * @returns Text with standardized Hijri symbols\n */\nexport const standardizeHijriSymbol = (text: string) => {\n // Replace standalone ู with ูู when it appears after Arabic digits (0-9 or ู -ูฉ)\n // Allow any amount of whitespace between the digit and ู, and consider Arabic punctuation as a boundary.\n // Boundary rule: only Arabic letters/digits should block replacement; punctuation should not.\n return text.replace(/([0-9\\u0660-\\u0669])\\s*ู(?=\\s|$|[^\\u0621-\\u063A\\u0641-\\u064A\\u0660-\\u0669])/gu, '$1 ูู');\n};\n\n/**\n * Standardizes standalone ุงู to ุงูู when appearing as whole word\n * @param text - Input text to process\n * @returns Text with standardized AH Hijri symbols\n */\nexport const standardizeIntahaSymbol = (text: string) => {\n // Replace standalone ุงู with ุงูู when it appears as a whole word\n // Ensures it's preceded by start/whitespace/non-Arabic AND followed by end/whitespace/non-Arabic\n return text.replace(/(^|\\s|[^\\u0600-\\u06FF])ุงู(?=\\s|$|[^\\u0600-\\u06FF])/gu, `$1${INTAHA_ACTUAL}`);\n};\n","import { PATTERNS } from './utils/textUtils';\n\nconst INVALID_FOOTNOTE = '()';\n\n/**\n * Checks if the given text contains invalid footnote references.\n * Invalid footnotes include empty parentheses \"()\" or OCR-confused characters\n * like \".1OV9\" that were misrecognized instead of Arabic numerals.\n *\n * @param text - Text to check for invalid footnote patterns\n * @returns True if text contains invalid footnote references, false otherwise\n * @example\n * hasInvalidFootnotes('This text has ()') // Returns true\n * hasInvalidFootnotes('This text has (ูก)') // Returns false\n * hasInvalidFootnotes('OCR mistake (O)') // Returns true\n */\nexport const hasInvalidFootnotes = (text: string): boolean => {\n return PATTERNS.invalidReferenceRegex.test(text);\n};\n\n// Arabic number formatter instance\nconst arabicFormatter = new Intl.NumberFormat('ar-SA');\n\n/**\n * Converts a number to Arabic-Indic numerals using the Intl.NumberFormat API.\n * Uses the 'ar-SA' locale to ensure proper Arabic numeral formatting.\n *\n * @param num - The number to convert to Arabic numerals\n * @returns String representation using Arabic-Indic digits (ู -ูฉ)\n * @example\n * numberToArabic(123) // Returns 'ูกูขูฃ'\n * numberToArabic(5) // Returns 'ูฅ'\n */\nconst numberToArabic = (num: number): string => {\n return arabicFormatter.format(num);\n};\n\n/**\n * Converts OCR-confused characters to their corresponding Arabic-Indic numerals.\n * Handles common OCR misrecognitions where Latin characters are mistaken for Arabic digits.\n *\n * @param char - Single character that may be an OCR mistake\n * @returns Corresponding Arabic-Indic numeral or original character if no mapping exists\n * @example\n * ocrToArabic('O') // Returns 'ูฅ' (O often confused with ูฅ)\n * ocrToArabic('1') // Returns 'ูก' (1 often confused with ูก)\n * ocrToArabic('.') // Returns 'ู ' (dot often confused with ู )\n */\nconst ocrToArabic = (char: string): string => {\n const ocrToArabicMap: { [key: string]: string } = {\n '1': 'ูก',\n '9': 'ูฉ',\n '.': 'ู ',\n O: 'ูฅ',\n o: 'ูฅ',\n V: 'ูง',\n v: 'ูง',\n };\n return ocrToArabicMap[char] || char;\n};\n\n/**\n * Parses Arabic-Indic numerals from a reference string and converts to a JavaScript number.\n * Removes parentheses and converts each Arabic-Indic digit to its Western equivalent.\n *\n * @param arabicStr - String containing Arabic-Indic numerals, typically in format '(ูกูขูฃ)'\n * @returns Parsed number, or 0 if parsing fails\n * @example\n * arabicToNumber('(ูกูขูฃ)') // Returns 123\n * arabicToNumber('(ูฅ)') // Returns 5\n * arabicToNumber('invalid') // Returns 0\n */\nconst arabicToNumber = (arabicStr: string): number => {\n const lookup: { [key: string]: string } = {\n 'ู ': '0',\n 'ูก': '1',\n 'ูข': '2',\n 'ูฃ': '3',\n 'ูค': '4',\n 'ูฅ': '5',\n 'ูฆ': '6',\n 'ูง': '7',\n 'ูจ': '8',\n 'ูฉ': '9',\n };\n const digits = arabicStr.replace(/[()]/g, '');\n let numStr = '';\n for (const char of digits) {\n numStr += lookup[char];\n }\n const parsed = parseInt(numStr, 10);\n return Number.isNaN(parsed) ? 0 : parsed;\n};\n\ntype TextLine = {\n isFootnote?: boolean;\n text: string;\n};\n\n/**\n * Extracts all footnote references from text lines, categorizing them by type and location.\n * Handles both Arabic-Indic numerals and OCR-confused characters in body text and footnotes.\n *\n * @param lines - Array of text line objects with optional isFootnote flag\n * @returns Object containing categorized reference arrays:\n * - bodyReferences: All valid references found in body text\n * - footnoteReferences: All valid references found in footnotes\n * - ocrConfusedInBody: OCR-confused references in body text (for tracking)\n * - ocrConfusedInFootnotes: OCR-confused references in footnotes (for tracking)\n * @example\n * const lines = [\n * { text: 'Body with (ูก) and (O)', isFootnote: false },\n * { text: '(ูก) Footnote text', isFootnote: true }\n * ];\n * const refs = extractReferences(lines);\n * // refs.bodyReferences contains ['(ูก)', '(ูฅ)'] - OCR 'O' converted to 'ูฅ'\n */\nconst extractReferences = (lines: TextLine[]) => {\n const arabicReferencesInBody = lines\n .filter((b) => !b.isFootnote)\n .flatMap((b) => b.text.match(PATTERNS.arabicReferenceRegex) || []);\n\n const ocrConfusedReferencesInBody = lines\n .filter((b) => !b.isFootnote)\n .flatMap((b) => b.text.match(PATTERNS.ocrConfusedReferenceRegex) || []);\n\n const arabicReferencesInFootnotes = lines\n .filter((b) => b.isFootnote)\n .flatMap((b) => b.text.match(PATTERNS.arabicFootnoteReferenceRegex) || []);\n\n const ocrConfusedReferencesInFootnotes = lines\n .filter((b) => b.isFootnote)\n .flatMap((b) => b.text.match(PATTERNS.ocrConfusedFootnoteReferenceRegex) || []);\n\n const convertedOcrBodyRefs = ocrConfusedReferencesInBody.map((ref) =>\n ref.replace(/[.1OV9]/g, (char) => ocrToArabic(char)),\n );\n\n const convertedOcrFootnoteRefs = ocrConfusedReferencesInFootnotes.map((ref) =>\n ref.replace(/[.1OV9]/g, (char) => ocrToArabic(char)),\n );\n\n return {\n bodyReferences: [...arabicReferencesInBody, ...convertedOcrBodyRefs],\n footnoteReferences: [...arabicReferencesInFootnotes, ...convertedOcrFootnoteRefs],\n ocrConfusedInBody: ocrConfusedReferencesInBody,\n ocrConfusedInFootnotes: ocrConfusedReferencesInFootnotes,\n };\n};\n\n/**\n * Determines if footnote reference correction is needed by checking for:\n * 1. Invalid footnote patterns (empty parentheses, OCR mistakes)\n * 2. Mismatched sets of references between body text and footnotes\n * 3. Different counts of references in body vs footnotes\n *\n * @param lines - Array of text line objects to analyze\n * @param references - Extracted reference data from extractReferences()\n * @returns True if correction is needed, false if references are already correct\n * @example\n * const lines = [{ text: 'Text with ()', isFootnote: false }];\n * const refs = extractReferences(lines);\n * needsCorrection(lines, refs) // Returns true due to invalid \"()\" reference\n */\nconst needsCorrection = (lines: TextLine[], references: ReturnType<typeof extractReferences>) => {\n const mistakenReferences = lines.some((line) => hasInvalidFootnotes(line.text));\n if (mistakenReferences) {\n return true;\n }\n\n const bodySet = new Set(references.bodyReferences);\n const footnoteSet = new Set(references.footnoteReferences);\n if (bodySet.size !== footnoteSet.size) {\n return true;\n }\n\n // Check if the sets contain the same elements\n for (const ref of bodySet) {\n if (!footnoteSet.has(ref)) {\n return true;\n }\n }\n\n return false;\n};\n\n/**\n * Corrects footnote references in an array of text lines by:\n * 1. Converting OCR-confused characters to proper Arabic numerals\n * 2. Filling in empty \"()\" references with appropriate numbers\n * 3. Ensuring footnote references in body text match those in footnotes\n * 4. Generating new reference numbers when needed\n *\n * @param lines - Array of text line objects, each with optional isFootnote flag\n * @returns Array of corrected text lines with proper footnote references\n * @example\n * const lines = [\n * { text: 'Main text with ()', isFootnote: false },\n * { text: '() This is a footnote', isFootnote: true }\n * ];\n * const corrected = correctReferences(lines);\n * // Returns lines with \"()\" replaced by proper Arabic numerals like \"(ูก)\"\n */\nexport const correctReferences = <T extends TextLine>(lines: T[]): T[] => {\n const initialReferences = extractReferences(lines);\n\n if (!needsCorrection(lines, initialReferences)) {\n return lines;\n }\n\n // Pass 1: Sanitize lines by correcting only OCR characters inside reference markers.\n const sanitizedLines = lines.map((line) => {\n let updatedText = line.text;\n // This regex finds the full reference, e.g., \"(O)\" or \"(1)\"\n const ocrRegex = /\\([.1OV9]+\\)/g;\n updatedText = updatedText.replace(ocrRegex, (match) => {\n // This replace acts *inside* the found match, e.g., on \"O\" or \"1\"\n return match.replace(/[.1OV9]/g, (char) => ocrToArabic(char));\n });\n return { ...line, text: updatedText };\n });\n\n // Pass 2: Analyze the sanitized lines to get a clear and accurate picture of references.\n const cleanReferences = extractReferences(sanitizedLines);\n\n // Step 3: Create queues of \"unmatched\" references for two-way pairing.\n const bodyRefSet = new Set(cleanReferences.bodyReferences);\n const footnoteRefSet = new Set(cleanReferences.footnoteReferences);\n\n const uniqueBodyRefs = [...new Set(cleanReferences.bodyReferences)];\n const uniqueFootnoteRefs = [...new Set(cleanReferences.footnoteReferences)];\n\n // Queue 1: Body references available for footnotes.\n const bodyRefsForFootnotes = uniqueBodyRefs.filter((ref) => !footnoteRefSet.has(ref));\n // Queue 2: Footnote references available for the body.\n const footnoteRefsForBody = uniqueFootnoteRefs.filter((ref) => !bodyRefSet.has(ref));\n\n // Step 4: Determine the starting point for any completely new reference numbers.\n const allRefs = [...bodyRefSet, ...footnoteRefSet];\n const maxRefNum = allRefs.length > 0 ? Math.max(0, ...allRefs.map((ref) => arabicToNumber(ref))) : 0;\n const referenceCounter = { count: maxRefNum + 1 };\n\n // Step 5: Map over the sanitized lines, filling in '()' using the queues.\n return sanitizedLines.map((line) => {\n if (!line.text.includes(INVALID_FOOTNOTE)) {\n return line;\n }\n let updatedText = line.text;\n\n updatedText = updatedText.replace(/\\(\\)/g, () => {\n if (line.isFootnote) {\n const availableRef = bodyRefsForFootnotes.shift();\n if (availableRef) {\n return availableRef;\n }\n } else {\n // It's body text\n const availableRef = footnoteRefsForBody.shift();\n if (availableRef) {\n return availableRef;\n }\n }\n\n // If no available partner reference exists, generate a new one.\n const newRef = `(${numberToArabic(referenceCounter.count)})`;\n referenceCounter.count++;\n return newRef;\n });\n\n return { ...line, text: updatedText };\n });\n};\n","/**\n * Node in the Aho-Corasick automaton trie structure.\n * Each node represents a state in the pattern matching automaton.\n */\nclass ACNode {\n /** Transition map from characters to next node indices */\n next: Map<string, number> = new Map();\n /** Failure link for efficient pattern matching */\n link = 0;\n /** Pattern IDs that end at this node */\n out: number[] = [];\n}\n\n/**\n * Aho-Corasick automaton for efficient multi-pattern string matching.\n * Provides O(n + m + z) time complexity where n is text length,\n * m is total pattern length, and z is number of matches.\n */\nexport class AhoCorasick {\n /** Array of nodes forming the automaton */\n private nodes: ACNode[] = [new ACNode()];\n\n /**\n * Adds a pattern to the automaton trie.\n *\n * @param pattern - Pattern string to add\n * @param id - Unique identifier for this pattern\n */\n add(pattern: string, id: number): void {\n let v = 0;\n for (let i = 0; i < pattern.length; i++) {\n const ch = pattern[i];\n let to = this.nodes[v].next.get(ch);\n if (to === undefined) {\n to = this.nodes.length;\n this.nodes[v].next.set(ch, to);\n this.nodes.push(new ACNode());\n }\n v = to;\n }\n this.nodes[v].out.push(id);\n }\n\n /**\n * Builds failure links for the automaton using BFS.\n * Must be called after adding all patterns and before searching.\n */\n build(): void {\n const q: number[] = [];\n for (const [, to] of this.nodes[0].next) {\n this.nodes[to].link = 0;\n q.push(to);\n }\n for (let qi = 0; qi < q.length; qi++) {\n const v = q[qi]!;\n\n for (const [ch, to] of this.nodes[v].next) {\n q.push(to);\n let link = this.nodes[v].link;\n while (link !== 0 && !this.nodes[link].next.has(ch)) {\n link = this.nodes[link].link;\n }\n const nxt = this.nodes[link].next.get(ch);\n this.nodes[to].link = nxt === undefined ? 0 : nxt;\n const linkOut = this.nodes[this.nodes[to].link].out;\n if (linkOut.length) {\n this.nodes[to].out.push(...linkOut);\n }\n }\n }\n }\n\n /**\n * Finds all pattern matches in the given text.\n *\n * @param text - Text to search in\n * @param onMatch - Callback function called for each match found\n * Receives pattern ID and end position of the match\n */\n find(text: string, onMatch: (patternId: number, endPos: number) => void): void {\n let v = 0;\n for (let i = 0; i < text.length; i++) {\n const ch = text[i];\n while (v !== 0 && !this.nodes[v].next.has(ch)) {\n v = this.nodes[v].link;\n }\n const to = this.nodes[v].next.get(ch);\n v = to === undefined ? 0 : to;\n if (this.nodes[v].out.length) {\n for (const pid of this.nodes[v].out) {\n onMatch(pid, i + 1);\n }\n }\n }\n }\n}\n\n/**\n * Builds Aho-Corasick automaton for exact pattern matching.\n *\n * @param patterns - Array of patterns to search for\n * @returns Constructed and built Aho-Corasick automaton ready for searching\n *\n * @example\n * ```typescript\n * const patterns = ['hello', 'world', 'hell'];\n * const ac = buildAhoCorasick(patterns);\n * ac.find('hello world', (patternId, endPos) => {\n * console.log(`Found pattern ${patternId} ending at position ${endPos}`);\n * });\n * ```\n */\nexport const buildAhoCorasick = (patterns: string[]) => {\n const ac = new AhoCorasick();\n for (let pid = 0; pid < patterns.length; pid++) {\n const pat = patterns[pid];\n if (pat.length > 0) {\n ac.add(pat, pid);\n }\n }\n ac.build();\n return ac;\n};\n","import type { MatchPolicy } from '@/types';\n\nexport const DEFAULT_POLICY: Required<MatchPolicy> = {\n enableFuzzy: true,\n gramsPerExcerpt: 5,\n log: () => {},\n maxCandidatesPerExcerpt: 40,\n maxEditAbs: 3,\n maxEditRel: 0.1,\n q: 4,\n seamLen: 512,\n};\n","import { buildAhoCorasick } from './ahocorasick';\nimport { boundedLevenshtein } from './levenshthein';\n\nconst SEAM_GAP_CEILING = 200; // max chars we are willing to skip at a boundary\nconst SEAM_BONUS_CAP = 80; // extra edit distance allowed for cross-page cases\n\n/**\n * Builds a concatenated book from pages with position tracking\n */\nexport function buildBook(pagesN: string[]) {\n const parts: string[] = [];\n const starts: number[] = [];\n const lens: number[] = [];\n let off = 0;\n\n for (let i = 0; i < pagesN.length; i++) {\n const p = pagesN[i];\n starts.push(off);\n lens.push(p.length);\n parts.push(p);\n off += p.length;\n\n if (i + 1 < pagesN.length) {\n parts.push(' '); // single space to allow cross-page substring matches\n off += 1;\n }\n }\n return { book: parts.join(''), lens, starts };\n}\n\n/**\n * Binary search to find which page contains a given position\n */\nexport function posToPage(pos: number, pageStarts: number[]): number {\n let lo = 0;\n let hi = pageStarts.length - 1;\n let ans = 0;\n\n while (lo <= hi) {\n const mid = (lo + hi) >> 1;\n if (pageStarts[mid] <= pos) {\n ans = mid;\n lo = mid + 1;\n } else {\n hi = mid - 1;\n }\n }\n return ans;\n}\n\n/**\n * Performs exact matching using Aho-Corasick algorithm to find all occurrences\n * of patterns in the concatenated book text.\n *\n * @param book - Concatenated text from all pages\n * @param pageStarts - Array of starting positions for each page in the book\n * @param patterns - Array of deduplicated patterns to search for\n * @param patIdToOrigIdxs - Mapping from pattern IDs to original excerpt indices\n * @param excerpts - Original array of excerpts (used for length reference)\n * @returns Object containing result array and exact match flags\n */\nexport function findExactMatches(\n book: string,\n pageStarts: number[],\n patterns: string[],\n patIdToOrigIdxs: number[][],\n excerptsCount: number,\n): { result: Int32Array; seenExact: Uint8Array } {\n const ac = buildAhoCorasick(patterns);\n const result = new Int32Array(excerptsCount).fill(-1);\n const seenExact = new Uint8Array(excerptsCount);\n\n ac.find(book, (pid, endPos) => {\n const pat = patterns[pid];\n const startPos = endPos - pat.length;\n const startPage = posToPage(startPos, pageStarts);\n\n for (const origIdx of patIdToOrigIdxs[pid]) {\n if (!seenExact[origIdx]) {\n result[origIdx] = startPage;\n seenExact[origIdx] = 1;\n }\n }\n });\n\n return { result, seenExact };\n}\n\n/**\n * Deduplicates excerpts and creates pattern mapping\n */\nexport function deduplicateExcerpts(excerptsN: string[]) {\n const keyToPatId = new Map<string, number>();\n const patIdToOrigIdxs: number[][] = [];\n const patterns: string[] = [];\n\n for (let i = 0; i < excerptsN.length; i++) {\n const k = excerptsN[i];\n let pid = keyToPatId.get(k);\n\n if (pid === undefined) {\n pid = patterns.length;\n keyToPatId.set(k, pid);\n patterns.push(k);\n patIdToOrigIdxs.push([i]);\n } else {\n patIdToOrigIdxs[pid].push(i);\n }\n }\n\n return { keyToPatId, patIdToOrigIdxs, patterns };\n}\n\n/**\n * Calculates fuzzy match score for a candidate using bounded Levenshtein distance.\n * Extracts a window around the candidate position and computes edit distance.\n *\n * @param excerpt - Text excerpt to match\n * @param candidate - Candidate position to evaluate\n * @param pagesN - Array of normalized page texts\n * @param seams - Array of seam data\n * @param maxDist - Maximum edit distance to consider\n * @returns Edit distance if within bounds, null otherwise\n */\nexport const calculateFuzzyScore = (\n excerpt: string,\n candidate: { page: number; seam: boolean; start: number },\n pagesN: string[],\n seams: { text: string }[],\n maxDist: number,\n) => {\n const L = excerpt.length;\n const extra = Math.min(maxDist, Math.max(6, Math.ceil(L * 0.12)));\n const half = Math.floor(extra / 2);\n const start0 = candidate.start - half;\n\n const base = candidate.seam ? seams[candidate.page]?.text : pagesN[candidate.page];\n if (!base) {\n return null;\n }\n\n const buildWindow = createWindowBuilder(candidate, pagesN, seams, start0, L, extra);\n const windows = generateWindows(buildWindow, candidate, base, start0, L, extra);\n\n const acceptance = calculateAcceptance(candidate, base, start0, L, extra, maxDist);\n return findBestMatch(windows, excerpt, acceptance);\n};\n\n/**\n * Creates a window builder function for the given candidate\n */\nconst createWindowBuilder = (\n candidate: { page: number; seam: boolean; start: number },\n pagesN: string[],\n seams: { text: string }[],\n start0: number,\n L: number,\n extra: number,\n) => {\n return (trimTailEndBy: number = 0, trimHeadStartBy: number = 0): string | null => {\n if (candidate.seam) {\n return buildSeamWindow(seams, candidate.page, start0, L, extra);\n }\n return buildPageWindow(pagesN, candidate.page, start0, L, extra, trimTailEndBy, trimHeadStartBy);\n };\n};\n\n/**\n * Builds a window from seam text\n */\nconst buildSeamWindow = (\n seams: { text: string }[],\n page: number,\n start0: number,\n L: number,\n extra: number,\n): string | null => {\n const seam = seams[page]?.text;\n if (!seam) {\n return null;\n }\n\n const s0 = Math.max(0, start0);\n const desired = L + extra;\n const end = Math.min(seam.length, s0 + desired);\n return end > s0 ? seam.slice(s0, end) : null;\n};\n\n/**\n * Builds a window from page text, potentially spanning multiple pages\n */\nconst buildPageWindow = (\n pagesN: string[],\n page: number,\n start0: number,\n L: number,\n extra: number,\n trimTailEndBy: number,\n trimHeadStartBy: number,\n): string | null => {\n const base = pagesN[page];\n if (!base) {\n return null;\n }\n\n const desired = L + extra;\n let s0 = start0;\n let window = '';\n\n // Prepend from previous pages if needed\n if (s0 < 0) {\n const needFromPrev = Math.max(0, -s0 - trimHeadStartBy);\n if (needFromPrev > 0) {\n window += buildPreviousPagesContent(pagesN, page, needFromPrev);\n }\n s0 = 0;\n }\n\n // Take from current page\n const end0 = Math.min(base.length - trimTailEndBy, Math.max(0, s0) + desired - window.length);\n if (end0 > s0) {\n window += base.slice(Math.max(0, s0), end0);\n }\n\n // Append from following pages\n window += buildFollowingPagesContent(pagesN, page, desired - window.length);\n\n return window.length ? window : null;\n};\n\n/**\n * Builds content from previous pages\n */\nconst buildPreviousPagesContent = (pagesN: string[], currentPage: number, needed: number): string => {\n let needPre = needed;\n let pp = currentPage - 1;\n const bits: string[] = [];\n\n while (needPre > 0 && pp >= 0) {\n const src = pagesN[pp];\n if (!src) {\n break;\n }\n\n const take = Math.min(needPre, src.length);\n const chunk = src.slice(src.length - take);\n bits.unshift(chunk);\n needPre -= chunk.length;\n pp--;\n }\n\n return bits.length ? `${bits.join(' ')} ` : '';\n};\n\n/**\n * Builds content from following pages\n */\nconst buildFollowingPagesContent = (pagesN: string[], currentPage: number, remaining: number): string => {\n let content = '';\n let pn = currentPage + 1;\n\n while (remaining > 0 && pn < pagesN.length) {\n const src = pagesN[pn];\n if (!src) {\n break;\n }\n\n const addition = src.slice(0, remaining);\n if (!addition.length) {\n break;\n }\n\n content += ` ${addition}`;\n remaining -= addition.length;\n pn++;\n }\n\n return content;\n};\n\n/**\n * Generates all possible windows for matching\n */\nconst generateWindows = (\n buildWindow: (trimTail: number, trimHead: number) => string | null,\n candidate: { page: number; seam: boolean; start: number },\n base: string,\n start0: number,\n L: number,\n extra: number,\n): string[] => {\n const windows: string[] = [];\n const desired = L + extra;\n const crossesEnd = !candidate.seam && start0 + desired > base.length;\n const crossesStart = !candidate.seam && start0 < 0;\n\n // Primary window\n const w0 = buildWindow(0, 0);\n if (w0) {\n windows.push(w0);\n }\n\n // Trimmed tail window if crossing end\n if (crossesEnd) {\n const cut = Math.min(SEAM_GAP_CEILING, Math.max(0, base.length - Math.max(0, start0)));\n if (cut > 0) {\n const wTrimTail = buildWindow(cut, 0);\n if (wTrimTail) {\n windows.push(wTrimTail);\n }\n }\n }\n\n // Trimmed head window if crossing start\n if (crossesStart) {\n const wTrimHead = buildWindow(0, Math.min(SEAM_GAP_CEILING, -start0));\n if (wTrimHead) {\n windows.push(wTrimHead);\n }\n }\n\n return windows;\n};\n\n/**\n * Calculates the acceptance threshold for edit distance\n */\nconst calculateAcceptance = (\n candidate: { page: number; seam: boolean; start: number },\n base: string,\n start0: number,\n L: number,\n extra: number,\n maxDist: number,\n): number => {\n const desired = L + extra;\n const crossesEnd = !candidate.seam && start0 + desired > base.length;\n const crossesStart = !candidate.seam && start0 < 0;\n\n const normalizationSlack = Math.min(2, Math.max(1, Math.ceil(L * 0.005)));\n\n return crossesEnd || crossesStart || candidate.seam\n ? maxDist + Math.min(SEAM_BONUS_CAP, Math.ceil(L * 0.08))\n : maxDist + normalizationSlack;\n};\n\n/**\n * Finds the best match among all windows\n */\nconst findBestMatch = (\n windows: string[],\n excerpt: string,\n acceptance: number,\n): { acceptance: number; dist: number } | null => {\n let best: number | null = null;\n\n for (const w of windows) {\n const d = boundedLevenshtein(excerpt, w, acceptance);\n if (d <= acceptance && (best == null || d < best)) {\n best = d;\n }\n }\n\n return best == null ? null : { acceptance, dist: best };\n};\n","/**\n * Represents a posting in the inverted index, storing position information.\n */\ntype Posting = {\n /** Page number where this gram occurs */\n page: number;\n /** Position within the page where this gram starts */\n pos: number;\n /** Whether this posting is from a seam (cross-page boundary) */\n seam: boolean;\n};\n\n/**\n * Basic gram information with position offset.\n */\ntype GramBase = {\n /** The q-gram string */\n gram: string;\n /** Offset position of this gram in the original text */\n offset: number;\n};\n\n/**\n * Extended gram information including frequency data for selection.\n */\ntype GramItem = GramBase & {\n /** Frequency count of this gram in the corpus */\n freq: number;\n};\n\n/**\n * Q-gram index for efficient fuzzy string matching candidate generation.\n * Maintains an inverted index of q-grams to their occurrence positions.\n */\nexport class QGramIndex {\n /** Length of q-grams to index */\n\n private q: number;\n /** Inverted index mapping q-grams to their postings */\n\n private map = new Map<string, Posting[]>();\n /** Frequency count for each q-gram in the corpus */\n\n private gramFreq = new Map<string, number>();\n\n /**\n * Creates a new Q-gram index with the specified gram length.\n * @param q - Length of q-grams to index (typically 3-5)\n */\n constructor(q: number) {\n this.q = q;\n }\n\n /**\n * Adds text to the index, extracting q-grams and building postings.\n *\n * @param page - Page number or identifier for this text\n * @param text - Text content to index\n * @param seam - Whether this text represents a seam (cross-page boundary)\n */\n addText(page: number, text: string, seam: boolean): void {\n const q = this.q;\n const m = text.length;\n if (m < q) {\n return;\n }\n\n for (let i = 0; i + q <= m; i++) {\n const gram = text.slice(i, i + q);\n\n // postings\n let postings = this.map.get(gram);\n if (!postings) {\n postings = [];\n this.map.set(gram, postings);\n }\n postings.push({ page, pos: i, seam });\n\n // freq\n this.gramFreq.set(gram, (this.gramFreq.get(gram) ?? 0) + 1);\n }\n }\n\n /**\n * Picks the rarest grams from an excerpt that exist in the index.\n */\n pickRare(excerpt: string, gramsPerExcerpt: number): { gram: string; offset: number }[] {\n gramsPerExcerpt = Math.max(1, Math.floor(gramsPerExcerpt));\n\n // extract unique grams with freqs (single pass)\n const items: GramItem[] = [];\n const seen = new Set<string>();\n const q = this.q;\n for (let i = 0; i + q <= excerpt.length; i++) {\n const gram = excerpt.slice(i, i + q);\n if (seen.has(gram)) {\n continue;\n }\n seen.add(gram);\n const freq = this.gramFreq.get(gram) ?? 0x7fffffff;\n items.push({ freq, gram, offset: i });\n }\n items.sort((a, b) => a.freq - b.freq);\n\n // prefer rare grams that exist; fallback to common ones if nothing exists\n const result: GramBase[] = [];\n for (const it of items) {\n if (this.map.has(it.gram)) {\n result.push({ gram: it.gram, offset: it.offset });\n if (result.length >= gramsPerExcerpt) {\n return result;\n }\n }\n }\n if (result.length < gramsPerExcerpt) {\n const chosen = new Set(result.map((r) => r.gram));\n for (let i = items.length - 1; i >= 0 && result.length < gramsPerExcerpt; i--) {\n const it = items[i]!;\n if (this.map.has(it.gram) && !chosen.has(it.gram)) {\n result.push({ gram: it.gram, offset: it.offset });\n chosen.add(it.gram);\n }\n }\n }\n return result;\n }\n\n getPostings(gram: string): Posting[] | undefined {\n return this.map.get(gram);\n }\n}\n","import type { MatchPolicy } from './types';\nimport { buildAhoCorasick } from './utils/ahocorasick';\nimport { DEFAULT_POLICY } from './utils/constants';\nimport { buildBook, calculateFuzzyScore, deduplicateExcerpts, findExactMatches, posToPage } from './utils/fuzzyUtils';\nimport { QGramIndex } from './utils/qgram';\nimport { sanitizeArabic } from './utils/sanitize';\n\n/**\n * Represents a candidate match position for fuzzy matching.\n */\ntype Candidate = {\n /** Page number where the candidate match is found */\n page: number;\n /** Starting position within the page or seam */\n start: number;\n /** Whether this candidate is from a seam (cross-page boundary) */\n seam: boolean;\n};\n\n/**\n * Data structure for cross-page text seams used in fuzzy matching.\n */\ntype SeamData = {\n /** Combined text from adjacent page boundaries */\n text: string;\n /** Starting page number for this seam */\n startPage: number;\n};\n\n/**\n * Represents a fuzzy match result with quality score.\n */\ntype FuzzyMatch = {\n /** Page number where the match was found */\n page: number;\n /** Edit distance (lower is better) */\n dist: number;\n};\n\n/**\n * Represents a page hit with quality metrics for ranking matches.\n */\ntype PageHit = {\n /** Quality score (0-1, higher is better) */\n score: number;\n /** Whether this is an exact match */\n exact: boolean;\n /** Whether this hit came from a seam candidate */\n seam: boolean;\n};\n\n/**\n * Creates seam data for cross-page matching by combining text from adjacent page boundaries.\n * Seams help find matches that span across page breaks.\n *\n * @param pagesN - Array of normalized page texts\n * @param seamLen - Length of text to take from each page boundary\n * @returns Array of seam data structures\n */\nfunction createSeams(pagesN: string[], seamLen: number): SeamData[] {\n const seams: SeamData[] = [];\n for (let p = 0; p + 1 < pagesN.length; p++) {\n const left = pagesN[p].slice(-seamLen);\n const right = pagesN[p + 1].slice(0, seamLen);\n const text = `${left} ${right}`;\n seams.push({ startPage: p, text });\n }\n return seams;\n}\n\n/**\n * Builds Q-gram index for efficient fuzzy matching candidate generation.\n * The index contains both regular pages and cross-page seams.\n *\n * @param pagesN - Array of normalized page texts\n * @param seams - Array of seam data for cross-page matching\n * @param q - Length of q-grams to index\n * @returns Constructed Q-gram index\n */\nfunction buildQGramIndex(pagesN: string[], seams: SeamData[], q: number): QGramIndex {\n const qidx = new QGramIndex(q);\n\n for (let p = 0; p < pagesN.length; p++) {\n qidx.addText(p, pagesN[p], false);\n }\n\n for (let p = 0; p < seams.length; p++) {\n qidx.addText(p, seams[p].text, true);\n }\n\n return qidx;\n}\n\n/**\n * Generates fuzzy matching candidates using rare q-grams from the excerpt.\n * Uses frequency-based selection to find the most discriminative grams.\n *\n * @param excerpt - Text excerpt to find candidates for\n * @param qidx - Q-gram index containing page and seam data\n * @param cfg - Match policy configuration\n * @returns Array of candidate match positions\n */\nfunction generateCandidates(excerpt: string, qidx: QGramIndex, cfg: Required<MatchPolicy>) {\n const seeds = qidx.pickRare(excerpt, cfg.gramsPerExcerpt);\n if (seeds.length === 0) {\n return [];\n }\n\n const candidates: Candidate[] = [];\n const seenKeys = new Set<string>();\n const excerptLen = excerpt.length;\n\n outer: for (const { gram, offset } of seeds) {\n const posts = qidx.getPostings(gram);\n if (!posts) {\n continue;\n }\n\n for (const p of posts) {\n const startPos = p.pos - offset;\n if (startPos < -Math.floor(excerptLen * 0.25)) {\n continue;\n }\n\n const start = Math.max(0, startPos);\n const key = `${p.page}:${start}:${p.seam ? 1 : 0}`;\n if (seenKeys.has(key)) {\n continue;\n }\n\n candidates.push({ page: p.page, seam: p.seam, start });\n seenKeys.add(key);\n\n if (candidates.length >= cfg.maxCandidatesPerExcerpt) {\n break outer;\n }\n }\n }\n\n return candidates;\n}\n\n/**\n * Finds the best fuzzy match among candidates by comparing edit distances.\n * Prioritizes lower edit distance, then earlier page number for tie-breaking.\n *\n * @param excerpt - Text excerpt to match\n * @param candidates - Array of candidate positions to evaluate\n * @param pagesN - Array of normalized page texts\n * @param seams - Array of seam data\n * @param cfg - Match policy configuration\n * @returns Best fuzzy match or null if none found\n */\nfunction findBestFuzzyMatch(\n excerpt: string,\n candidates: Candidate[],\n pagesN: string[],\n seams: SeamData[],\n cfg: Required<MatchPolicy>,\n): FuzzyMatch | null {\n if (excerpt.length === 0) {\n return null;\n }\n\n const maxDist = calculateMaxDistance(excerpt, cfg);\n cfg.log('maxDist', maxDist);\n\n const keyset = new Set<string>();\n let best: FuzzyMatch | null = null;\n\n for (const candidate of candidates) {\n if (shouldSkipCandidate(candidate, keyset)) {\n continue;\n }\n\n const match = evaluateCandidate(candidate, excerpt, pagesN, seams, maxDist, cfg);\n if (!match) {\n continue;\n }\n\n best = updateBestMatch(best, match, candidate);\n cfg.log('findBest best', best);\n\n if (match.dist === 0) {\n break;\n }\n }\n\n return best;\n}\n\n/**\n * Calculates the maximum edit distance allowed for a fuzzy comparison.\n *\n * @param excerpt - The excerpt currently being matched.\n * @param cfg - The resolved matching policy in effect.\n * @returns The maximum permitted edit distance for the excerpt.\n */\nfunction calculateMaxDistance(excerpt: string, cfg: Required<MatchPolicy>): number {\n return Math.max(cfg.maxEditAbs, Math.ceil(cfg.maxEditRel * excerpt.length));\n}\n\n/**\n * Checks whether a candidate has already been processed and should be skipped.\n *\n * @param candidate - The candidate under consideration.\n * @param keyset - A set of serialized candidate keys used for deduplication.\n * @returns True if the candidate was seen before, otherwise false.\n */\nfunction shouldSkipCandidate(candidate: Candidate, keyset: Set<string>): boolean {\n const key = `${candidate.page}:${candidate.start}:${candidate.seam ? 1 : 0}`;\n if (keyset.has(key)) {\n return true;\n }\n keyset.add(key);\n return false;\n}\n\n/**\n * Evaluates a candidate by computing its fuzzy score and checking acceptance.\n *\n * @param candidate - The candidate segment to evaluate.\n * @param excerpt - The normalized excerpt being matched.\n * @param pagesN - Normalized page content collection.\n * @param seams - Precomputed seam data for cross-page matching.\n * @param maxDist - Maximum allowed edit distance for this excerpt.\n * @param cfg - The resolved matching policy.\n * @returns The candidate's distance and acceptance threshold if valid, otherwise null.\n */\nfunction evaluateCandidate(\n candidate: Candidate,\n excerpt: string,\n pagesN: string[],\n seams: SeamData[],\n maxDist: number,\n cfg: Required<MatchPolicy>,\n): { dist: number; acceptance: number } | null {\n const res = calculateFuzzyScore(excerpt, candidate, pagesN, seams, maxDist);\n const dist = res?.dist ?? null;\n const acceptance = res?.acceptance ?? maxDist;\n\n cfg.log('dist', dist);\n\n return isValidMatch(dist, acceptance) ? { acceptance, dist: dist! } : null;\n}\n\n/**\n * Determines whether an evaluated match satisfies its acceptance threshold.\n *\n * @param dist - The computed edit distance for the match.\n * @param acceptance - The maximum acceptable distance for the match.\n * @returns True when the match should be accepted.\n */\nfunction isValidMatch(dist: number | null, acceptance: number): boolean {\n return dist !== null && dist <= acceptance;\n}\n\n/**\n * Updates the running \"best\" match if the current candidate improves it.\n *\n * @param current - The previously best fuzzy match, if any.\n * @param match - The latest candidate match metrics.\n * @param candidate - The candidate metadata associated with {@link match}.\n * @returns The preferred match after considering the candidate.\n */\nfunction updateBestMatch(\n current: FuzzyMatch | null,\n match: { dist: number; acceptance: number },\n candidate: Candidate,\n): FuzzyMatch {\n const newMatch = { dist: match.dist, page: candidate.page };\n\n if (!current) {\n return newMatch;\n }\n\n return isBetterMatch(match.dist, candidate.page, current.dist, current.page) ? newMatch : current;\n}\n\n/**\n * Determines whether a new match outranks the current best match.\n *\n * @param newDist - Edit distance for the new match.\n * @param newPage - Page index where the new match resides.\n * @param bestDist - Edit distance of the existing best match.\n * @param bestPage - Page index of the existing best match.\n * @returns True if the new match should replace the current best match.\n */\nfunction isBetterMatch(newDist: number, newPage: number, bestDist: number, bestPage: number): boolean {\n return newDist < bestDist || (newDist === bestDist && newPage < bestPage);\n}\n\n/**\n * Performs fuzzy matching for excerpts that didn't have exact matches.\n * Uses Q-gram indexing and bounded Levenshtein distance for efficiency.\n *\n * @param excerptsN - Array of normalized excerpts\n * @param pagesN - Array of normalized page texts\n * @param seenExact - Flags indicating which excerpts had exact matches\n * @param result - Result array to update with fuzzy match pages\n * @param cfg - Match policy configuration\n */\nfunction performFuzzyMatching(\n excerptsN: string[],\n pagesN: string[],\n seenExact: Uint8Array,\n result: Int32Array,\n cfg: Required<MatchPolicy>,\n): void {\n if (!cfg.enableFuzzy) {\n return;\n }\n\n const seams = createSeams(pagesN, cfg.seamLen);\n const qidx = buildQGramIndex(pagesN, seams, cfg.q);\n\n for (let i = 0; i < excerptsN.length; i++) {\n if (seenExact[i]) {\n continue;\n }\n\n const excerpt = excerptsN[i];\n cfg.log('excerpt', excerpt);\n if (!excerpt || excerpt.length < cfg.q) {\n continue;\n }\n\n const candidates = generateCandidates(excerpt, qidx, cfg);\n cfg.log('candidates', candidates);\n if (candidates.length === 0) {\n continue;\n }\n\n const best = findBestFuzzyMatch(excerpt, candidates, pagesN, seams, cfg);\n cfg.log('best', best);\n if (best) {\n result[i] = best.page;\n seenExact[i] = 1;\n }\n }\n}\n\n/**\n * Main function to find the single best match per excerpt.\n * Combines exact matching with fuzzy matching for comprehensive text search.\n *\n * @param pages - Array of page texts to search within\n * @param excerpts - Array of text excerpts to find matches for\n * @param policy - Optional matching policy configuration\n * @returns Array of page indices (one per excerpt, -1 if no match found)\n *\n * @example\n * ```typescript\n * const pages = ['Hello world', 'Goodbye world'];\n * const excerpts = ['Hello', 'Good bye']; // Note the typo\n * const matches = findMatches(pages, excerpts, { enableFuzzy: true });\n * // Returns [0, 1] - exact match on page 0, fuzzy match on page 1\n * ```\n */\nexport function findMatches(pages: string[], excerpts: string[], policy: MatchPolicy = {}) {\n const cfg = { ...DEFAULT_POLICY, ...policy };\n\n const pagesN = pages.map((p) => sanitizeArabic(p, 'aggressive'));\n const excerptsN = excerpts.map((e) => sanitizeArabic(e, 'aggressive'));\n\n if (policy.log) {\n policy.log('pages', pages);\n policy.log('excerpts', excerpts);\n policy.log('pagesN', pagesN);\n policy.log('excerptsN', excerptsN);\n }\n\n const { patIdToOrigIdxs, patterns } = deduplicateExcerpts(excerptsN);\n const { book, starts: pageStarts } = buildBook(pagesN);\n\n const { result, seenExact } = findExactMatches(book, pageStarts, patterns, patIdToOrigIdxs, excerpts.length);\n\n if (policy.log) {\n policy.log('findExactMatches result', result);\n policy.log('seenExact', seenExact);\n }\n\n if (!seenExact.every((seen) => seen === 1)) {\n performFuzzyMatching(excerptsN, pagesN, seenExact, result, cfg);\n }\n\n if (policy.log) {\n policy.log('performFuzzyMatching result', result);\n }\n\n return Array.from(result);\n}\n\n/**\n * Records exact matches for the findMatchesAll function.\n * Updates the hits tracking structure with exact match information.\n *\n * @param book - Concatenated text from all pages\n * @param pageStarts - Array of starting positions for each page\n * @param patterns - Array of deduplicated patterns to search for\n * @param patIdToOrigIdxs - Mapping from pattern IDs to original excerpt indices\n * @param hitsByExcerpt - Array of maps tracking hits per excerpt\n */\nfunction recordExactMatches(\n book: string,\n pageStarts: number[],\n patterns: string[],\n patIdToOrigIdxs: number[][],\n hitsByExcerpt: Array<Map<number, PageHit>>,\n): void {\n const ac = buildAhoCorasick(patterns);\n\n ac.find(book, (pid, endPos) => {\n const pat = patterns[pid];\n const startPos = endPos - pat.length;\n const startPage = posToPage(startPos, pageStarts);\n\n for (const origIdx of patIdToOrigIdxs[pid]) {\n const hits = hitsByExcerpt[origIdx];\n const prev = hits.get(startPage);\n if (!prev || !prev.exact) {\n hits.set(startPage, { exact: true, score: 1, seam: false });\n }\n }\n });\n}\n\n/**\n * Processes a single fuzzy candidate and updates hits if a better match is found.\n * Used internally by the findMatchesAll function for comprehensive matching.\n *\n * @param candidate - Candidate position to evaluate\n * @param excerpt - Text excerpt being matched\n * @param pagesN - Array of normalized page texts\n * @param seams - Array of seam data\n * @param maxDist - Maximum edit distance threshold\n * @param hits - Map of page hits to update\n * @param keyset - Set to track processed candidates (for deduplication)\n */\nfunction processFuzzyCandidate(\n candidate: Candidate,\n excerpt: string,\n pagesN: string[],\n seams: SeamData[],\n maxDist: number,\n hits: Map<number, PageHit>,\n keyset: Set<string>,\n): void {\n const key = `${candidate.page}:${candidate.start}:${candidate.seam ? 1 : 0}`;\n if (keyset.has(key)) {\n return;\n }\n keyset.add(key);\n\n const res = calculateFuzzyScore(excerpt, candidate, pagesN, seams, maxDist);\n if (!res) {\n return;\n }\n\n const { dist, acceptance } = res;\n if (dist > acceptance) {\n return;\n }\n\n const score = 1 - dist / acceptance;\n\n const entry = hits.get(candidate.page);\n if (!entry || (!entry.exact && score > entry.score)) {\n hits.set(candidate.page, { exact: false, score, seam: candidate.seam });\n }\n}\n\n/**\n * Processes fuzzy matching for a single excerpt in the findMatchesAll function.\n * Generates candidates and evaluates them for potential matches.\n *\n * @param excerptIndex - Index of the excerpt being processed\n * @param excerpt - Text excerpt to find matches for\n * @param pagesN - Array of normalized page texts\n * @param seams - Array of seam data\n * @param qidx - Q-gram index for candidate generation\n * @param hitsByExcerpt - Array of maps tracking hits per excerpt\n * @param cfg - Match policy configuration\n */\nfunction processSingleExcerptFuzzy(\n excerptIndex: number,\n excerpt: string,\n pagesN: string[],\n seams: SeamData[],\n qidx: QGramIndex,\n hitsByExcerpt: Array<Map<number, PageHit>>,\n cfg: Required<MatchPolicy>,\n): void {\n // Skip if we already have exact hits\n const hasExactHits = Array.from(hitsByExcerpt[excerptIndex].values()).some((v) => v.exact);\n if (hasExactHits) {\n return;\n }\n\n if (!excerpt || excerpt.length < cfg.q) {\n return;\n }\n\n const candidates = generateCandidates(excerpt, qidx, cfg);\n if (candidates.length === 0) {\n return;\n }\n\n const maxDist = Math.max(cfg.maxEditAbs, Math.ceil(cfg.maxEditRel * excerpt.length));\n const keyset = new Set<string>();\n const hits = hitsByExcerpt[excerptIndex];\n\n for (const candidate of candidates) {\n processFuzzyCandidate(candidate, excerpt, pagesN, seams, maxDist, hits, keyset);\n }\n}\n\n/**\n * Records fuzzy matches for excerpts that don't have exact matches.\n * Used by findMatchesAll to provide comprehensive matching results.\n *\n * @param excerptsN - Array of normalized excerpts\n * @param pagesN - Array of normalized page texts\n * @param hitsByExcerpt - Array of maps tracking hits per excerpt\n * @param cfg - Match policy configuration\n */\nfunction recordFuzzyMatches(\n excerptsN: string[],\n pagesN: string[],\n hitsByExcerpt: Array<Map<number, PageHit>>,\n cfg: Required<MatchPolicy>,\n): void {\n const seams = createSeams(pagesN, cfg.seamLen);\n const qidx = buildQGramIndex(pagesN, seams, cfg.q);\n\n for (let i = 0; i < excerptsN.length; i++) {\n processSingleExcerptFuzzy(i, excerptsN[i], pagesN, seams, qidx, hitsByExcerpt, cfg);\n }\n}\n\n/**\n * Sorts matches by quality and page order for optimal ranking.\n * Exact matches are prioritized over fuzzy matches, with secondary sorting by page order.\n *\n * @param hits - Map of page hits with quality scores\n * @returns Array of page numbers sorted by match quality\n */\nconst sortMatches = (hits: Map<number, PageHit>) => {\n if (hits.size === 0) {\n return [];\n }\n\n // 1) Collapse adjacent seam pairs: keep the stronger seam and drop the weaker neighbor.\n collapseAdjacentSeams(hits);\n\n // 2) Remove seam hits that are worse than a non-seam neighbor on the following page.\n removeWeakSeams(hits);\n\n // 3) Split and rank: exact first in reading order; then fuzzy by score desc, then page asc.\n return rankHits(hits);\n};\n\n/**\n * Removes weaker seam matches from adjacent seam pairs.\n *\n * @param hits - Mutable map of page hits that may contain seam entries.\n */\nconst collapseAdjacentSeams = (hits: Map<number, PageHit>) => {\n const pagesAsc = Array.from(hits.keys()).sort((a, b) => a - b);\n\n for (const page of pagesAsc) {\n const currentHit = hits.get(page);\n const nextHit = hits.get(page + 1);\n\n if (shouldCollapseSeams(currentHit, nextHit)) {\n const pageToRemove = selectWeakerSeam(page, currentHit!, nextHit!);\n hits.delete(pageToRemove);\n }\n }\n};\n\n/**\n * Checks whether two neighboring hits are both seams that should be merged.\n *\n * @param hit1 - The first hit in the pair.\n * @param hit2 - The second hit in the pair.\n * @returns True if both hits represent seam matches.\n */\nconst shouldCollapseSeams = (hit1?: PageHit, hit2?: PageHit): boolean => {\n return Boolean(hit1?.seam && hit2?.seam);\n};\n\n/**\n * Selects which seam page to discard based on score ordering.\n *\n * @param page1 - The page index for the first seam hit.\n * @param hit1 - The first seam hit entry.\n * @param hit2 - The second seam hit entry on the following page.\n * @returns The page index that should be removed from the hits map.\n */\nconst selectWeakerSeam = (page1: number, hit1: PageHit, hit2: PageHit): number => {\n if (hit2.score > hit1.score) {\n return page1;\n }\n if (hit2.score < hit1.score) {\n return page1 + 1;\n }\n return page1 + 1; // Tie: prefer earlier page\n};\n\n/**\n * Removes seam hits that are redundant compared to their neighbors.\n *\n * @param hits - Mutable map of page hits that may contain redundant seams.\n */\nconst removeWeakSeams = (hits: Map<number, PageHit>) => {\n const seamPages = Array.from(hits.entries())\n .filter(([, hit]) => hit.seam)\n .map(([page]) => page);\n\n for (const page of seamPages) {\n const seamHit = hits.get(page)!;\n const neighbor = hits.get(page + 1);\n\n if (isSeamRedundant(seamHit, neighbor)) {\n hits.delete(page);\n }\n }\n};\n\n/**\n * Determines whether a seam hit is redundant when compared to its neighbor.\n *\n * @param seamHit - The seam hit currently under review.\n * @param neighbor - The neighboring hit on the following page, if any.\n * @returns True if the seam hit should be removed.\n */\nconst isSeamRedundant = (seamHit: PageHit, neighbor?: PageHit): boolean => {\n if (!neighbor) {\n return false;\n }\n return neighbor.exact || (!neighbor.seam && neighbor.score >= seamHit.score);\n};\n\n/**\n * Splits hits into exact and fuzzy categories, then sorts and merges them.\n *\n * @param hits - Map of page hits to rank.\n * @returns Sorted page numbers ordered by relevance.\n */\nconst rankHits = (hits: Map<number, PageHit>): number[] => {\n const exact: [number, PageHit][] = [];\n const fuzzy: [number, PageHit][] = [];\n\n for (const entry of hits.entries()) {\n if (entry[1].exact) {\n exact.push(entry);\n } else {\n fuzzy.push(entry);\n }\n }\n\n exact.sort((a, b) => a[0] - b[0]);\n fuzzy.sort((a, b) => b[1].score - a[1].score || a[0] - b[0]);\n\n return [...exact, ...fuzzy].map((entry) => entry[0]);\n};\n\n/**\n * Main function to find all matches per excerpt, ranked by quality.\n * Returns comprehensive results with both exact and fuzzy matches for each excerpt.\n *\n * @param pages - Array of page texts to search within\n * @param excerpts - Array of text excerpts to find matches for\n * @param policy - Optional matching policy configuration\n * @returns Array of page index arrays (one array per excerpt, sorted by match quality)\n *\n * @example\n * ```typescript\n * const pages = ['Hello world', 'Hello there', 'Goodbye world'];\n * const excerpts = ['Hello'];\n * const matches = findMatchesAll(pages, excerpts);\n * // Returns [[0, 1]] - both pages 0 and 1 contain \"Hello\", sorted by page order\n * ```\n */\nexport function findMatchesAll(pages: string[], excerpts: string[], policy: MatchPolicy = {}): number[][] {\n const cfg = { ...DEFAULT_POLICY, ...policy };\n\n const pagesN = pages.map((p) => sanitizeArabic(p, 'aggressive'));\n const excerptsN = excerpts.map((e) => sanitizeArabic(e, 'aggressive'));\n\n if (policy.log) {\n policy.log('pages', pages);\n policy.log('excerpts', excerpts);\n policy.log('pagesN', pagesN);\n policy.log('excerptsN', excerptsN);\n }\n\n const { patIdToOrigIdxs, patterns } = deduplicateExcerpts(excerptsN);\n const { book, starts: pageStarts } = buildBook(pagesN);\n\n // Initialize hit tracking\n const hitsByExcerpt: Array<Map<number, PageHit>> = Array.from({ length: excerpts.length }, () => new Map());\n\n // Record exact matches\n recordExactMatches(book, pageStarts, patterns, patIdToOrigIdxs, hitsByExcerpt);\n\n // Record fuzzy matches if enabled\n if (cfg.enableFuzzy) {\n recordFuzzyMatches(excerptsN, pagesN, hitsByExcerpt, cfg);\n }\n\n // Sort and return results\n return hitsByExcerpt.map((hits) => sortMatches(hits));\n}\n","import { PATTERNS } from './utils/textUtils';\n\n/**\n * Character statistics for analyzing text content and patterns\n */\ntype CharacterStats = {\n /** Number of Arabic script characters in the text */\n arabicCount: number;\n /** Map of character frequencies for repetition analysis */\n charFreq: Map<string, number>;\n /** Number of digit characters (0-9) in the text */\n digitCount: number;\n /** Number of Latin alphabet characters (a-z, A-Z) in the text */\n latinCount: number;\n /** Number of punctuation characters in the text */\n punctuationCount: number;\n /** Number of whitespace characters in the text */\n spaceCount: number;\n /** Number of symbol characters (non-alphanumeric, non-punctuation) in the text */\n symbolCount: number;\n};\n\n/**\n * Determines if a given Arabic text string is likely to be noise or unwanted OCR artifacts.\n * This function performs comprehensive analysis to identify patterns commonly associated\n * with OCR errors, formatting artifacts, or meaningless content in Arabic text processing.\n *\n * @param text - The input string to analyze for noise patterns\n * @returns true if the text is likely noise or unwanted content, false if it appears to be valid Arabic content\n *\n * @example\n * ```typescript\n * import { isArabicTextNoise } from 'baburchi';\n *\n * console.log(isArabicTextNoise('---')); // true (formatting artifact)\n * console.log(isArabicTextNoise('ุงูุณูุงู
ุนูููู
')); // false (valid Arabic)\n * console.log(isArabicTextNoise('ABC')); // true (uppercase pattern)\n * ```\n */\nexport const isArabicTextNoise = (text: string): boolean => {\n // Early return for empty or very short strings\n if (!text || text.trim().length === 0) {\n return true;\n }\n\n const trimmed = text.trim();\n const length = trimmed.length;\n\n // Very short strings are likely noise unless they're meaningful Arabic\n if (length < 2) {\n return true;\n }\n\n // Check for basic noise patterns first\n if (isBasicNoisePattern(trimmed)) {\n return true;\n }\n\n const charStats = analyzeCharacterStats(trimmed);\n\n // Check for excessive repetition\n if (hasExcessiveRepetition(charStats, length)) {\n return true;\n }\n\n // Check if text contains Arabic characters\n const hasArabic = PATTERNS.arabicCharacters.test(trimmed);\n\n // Handle non-Arabic text\n if (!hasArabic && /[a-zA-Z]/.test(trimmed)) {\n return true;\n }\n\n // Arabic-specific validation\n if (hasArabic) {\n return !isValidArabicContent(charStats, length);\n }\n\n // Non-Arabic content validation\n return isNonArabicNoise(charStats, length, trimmed);\n};\n\n/**\n * Analyzes character composition and frequency statistics for the input text.\n * Categorizes characters by type (Arabic, Latin, digits, spaces, punctuation, symbols)\n * and tracks character frequency for pattern analysis.\n *\n * @param text - The text string to analyze\n * @returns CharacterStats object containing detailed character analysis\n *\n * @example\n * ```typescript\n * import { analyzeCharacterStats } from 'baburchi';\n *\n * const stats = analyzeCharacterStats('ู
ุฑุญุจุง 123!');\n * console.log(stats.arabicCount); // 5\n * console.log(stats.digitCount); // 3\n * console.log(stats.symbolCount); // 1\n * ```\n */\nexport function analyzeCharacterStats(text: string): CharacterStats {\n const stats: CharacterStats = {\n arabicCount: 0,\n charFreq: new Map<string, number>(),\n digitCount: 0,\n latinCount: 0,\n punctuationCount: 0,\n spaceCount: 0,\n symbolCount: 0,\n };\n\n const chars = Array.from(text);\n\n for (const char of chars) {\n // Count character frequency for repetition detection\n stats.charFreq.set(char, (stats.charFreq.get(char) || 0) + 1);\n\n if (PATTERNS.arabicCharacters.test(char)) {\n stats.arabicCount++;\n } else if (/\\d/.test(char)) {\n stats.digitCount++;\n } else if (/[a-zA-Z]/.test(char)) {\n stats.latinCount++;\n } else if (/\\s/.test(char)) {\n stats.spaceCount++;\n } else if (/[.,;:()[\\]{}\"\"\"''`]/.test(char)) {\n stats.punctuationCount++;\n } else {\n stats.symbolCount++;\n }\n }\n\n return stats;\n}\n\n/**\n * Detects excessive repetition of specific characters that commonly indicate noise.\n * Focuses on repetitive characters like exclamation marks, dots, dashes, equals signs,\n * and underscores that often appear in OCR artifacts or formatting elements.\n *\n * @param charStats - Character statistics from analyzeCharacterStats\n * @param textLength - Total length of the original text\n * @returns true if excessive repetition is detected, false otherwise\n *\n * @example\n * ```typescript\n * import { hasExcessiveRepetition, analyzeCharacterStats } from 'baburchi';\n *\n * const stats = analyzeCharacterStats('!!!!!');\n * console.log(hasExcessiveRepetition(stats, 5)); // true\n *\n * const normalStats = analyzeCharacterStats('hello world');\n * console.log(hasExcessiveRepetition(normalStats, 11)); // false\n * ```\n */\nexport function hasExcessiveRepetition(charStats: CharacterStats, textLength: number): boolean {\n let repeatCount = 0;\n const repetitiveChars = ['!', '.', '-', '=', '_'];\n\n for (const [char, count] of charStats.charFreq) {\n if (count >= 5 && repetitiveChars.includes(char)) {\n repeatCount += count;\n }\n }\n\n // High repetition ratio indicates noise\n return repeatCount / textLength > 0.4;\n}\n\n/**\n * Identifies text that matches common noise patterns using regular expressions.\n * Detects patterns like repeated dashes, dot sequences, uppercase-only text,\n * digit-dash combinations, and other formatting artifacts commonly found in OCR output.\n *\n * @param text - The text string to check against noise patterns\n * @returns true if the text matches a basic noise pattern, false otherwise\n *\n * @example\n * ```typescript\n * import { isBasicNoisePattern } from 'baburchi';\n *\n * console.log(isBasicNoisePattern('---')); // true\n * console.log(isBasicNoisePattern('...')); // true\n * console.log(isBasicNoisePattern('ABC')); // true\n * console.log(isBasicNoisePattern('- 77')); // true\n * console.log(isBasicNoisePattern('hello world')); // false\n * ```\n */\nexport function isBasicNoisePattern(text: string): boolean {\n const noisePatterns = [\n /^[-=_โโบโป\\s]*$/, // Only dashes, equals, underscores, special chars, or spaces\n /^[.\\s]*$/, // Only dots and spaces\n /^[!\\s]*$/, // Only exclamation marks and spaces\n /^[A-Z\\s]*$/, // Only uppercase letters and spaces (like \"Ap Ap Ap\")\n /^[-\\d\\s]*$/, // Only dashes, digits and spaces (like \"- 77\", \"- 4\")\n /^\\d+\\s*$/, // Only digits and spaces (like \"1\", \" 1 \")\n /^[A-Z]\\s*$/, // Single uppercase letter with optional spaces\n /^[โ\\s]*$/, // Only em-dashes and spaces\n /^[เฅเคฐ\\s-]*$/, // Devanagari characters (likely OCR errors)\n ];\n\n return noisePatterns.some((pattern) => pattern.test(text));\n}\n\n/**\n * Determines if non-Arabic content should be classified as noise based on various heuristics.\n * Analyzes symbol-to-content ratios, text length, spacing patterns, and content composition\n * to identify unwanted OCR artifacts or meaningless content.\n *\n * @param charStats - Character statistics from analyzeCharacterStats\n * @param textLength - Total length of the original text\n * @param text - The original text string for additional pattern matching\n * @returns true if the content is likely noise, false if it appears to be valid content\n *\n * @example\n * ```typescript\n * import { isNonArabicNoise, analyzeCharacterStats } from 'baburchi';\n *\n * const stats = analyzeCharacterStats('!!!');\n * console.log(isNonArabicNoise(stats, 3, '!!!')); // true\n *\n * const validStats = analyzeCharacterStats('2023');\n * console.log(isNonArabicNoise(validStats, 4, '2023')); // false\n * ```\n */\nexport function isNonArabicNoise(charStats: CharacterStats, textLength: number, text: string): boolean {\n const contentChars = charStats.arabicCount + charStats.latinCount + charStats.digitCount;\n\n // Text that's mostly symbols or punctuation is likely noise\n if (contentChars === 0) {\n return true;\n }\n\n // Check for specific spacing patterns that indicate noise\n if (isSpacingNoise(charStats, contentChars, textLength)) {\n return true;\n }\n\n // Special handling for Arabic numerals in parentheses (like \"(ูฆู ูกู ).\")\n const hasArabicNumerals = /[ู -ูฉ]/.test(text);\n if (hasArabicNumerals && charStats.digitCount >= 3) {\n return false;\n }\n\n // High symbol-to-content ratio indicates noise, but be more lenient with punctuation\n // Allow more punctuation for valid content like references, citations, etc.\n const adjustedNonContentChars = charStats.symbolCount + Math.max(0, charStats.punctuationCount - 5);\n if (adjustedNonContentChars / Math.max(contentChars, 1) > 2) {\n return true;\n }\n\n // Very short strings with no Arabic are likely noise (except substantial numbers)\n if (textLength <= 5 && charStats.arabicCount === 0 && !(/^\\d+$/.test(text) && charStats.digitCount >= 3)) {\n return true;\n }\n\n // Allow pure numbers if they're substantial (like years)\n if (/^\\d{3,4}$/.test(text)) {\n return false;\n }\n\n // Default to not noise for longer content\n return textLength <= 10;\n}\n\n/**\n * Detects problematic spacing patterns that indicate noise or OCR artifacts.\n * Identifies cases where spacing is excessive relative to content, or where\n * single characters are surrounded by spaces in a way that suggests OCR errors.\n *\n * @param charStats - Character statistics from analyzeCharacterStats\n * @param contentChars - Number of meaningful content characters (Arabic + Latin + digits)\n * @param textLength - Total length of the original text\n * @returns true if spacing patterns indicate noise, false otherwise\n *\n * @example\n * ```typescript\n * import { isSpacingNoise, analyzeCharacterStats } from 'baburchi';\n *\n * const stats = analyzeCharacterStats(' a ');\n * const contentChars = stats.arabicCount + stats.latinCount + stats.digitCount;\n * console.log(isSpacingNoise(stats, contentChars, 3)); // true\n *\n * const normalStats = analyzeCharacterStats('hello world');\n * const normalContent = normalStats.arabicCount + normalStats.latinCount + normalStats.digitCount;\n * console.log(isSpacingNoise(normalStats, normalContent, 11)); // false\n * ```\n */\nexport function isSpacingNoise(charStats: CharacterStats, contentChars: number, textLength: number): boolean {\n const { arabicCount, spaceCount } = charStats;\n\n // Too many spaces relative to content\n if (spaceCount > 0 && contentChars === spaceCount + 1 && contentChars <= 5) {\n return true;\n }\n\n // Short text with multiple spaces and no Arabic\n if (textLength <= 10 && spaceCount >= 2 && arabicCount === 0) {\n return true;\n }\n\n // Excessive spacing ratio\n if (spaceCount / textLength > 0.6) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Validates whether Arabic content is substantial enough to be considered meaningful.\n * Uses character counts and text length to determine if Arabic text contains\n * sufficient content or if it's likely to be a fragment or OCR artifact.\n *\n * @param charStats - Character statistics from analyzeCharacterStats\n * @param textLength - Total length of the original text\n * @returns true if the Arabic content appears valid, false if it's likely noise\n *\n * @example\n * ```typescript\n * import { isValidArabicContent, analyzeCharacterStats } from 'baburchi';\n *\n * const validStats = analyzeCharacterStats('ุงูุณูุงู
ุนูููู
');\n * console.log(isValidArabicContent(validStats, 12)); // true\n *\n * const shortStats = analyzeCharacterStats('ุต');\n * console.log(isValidArabicContent(shortStats, 1)); // false\n *\n * const withDigitsStats = analyzeCharacterStats('ุต 5');\n * console.log(isValidArabicContent(withDigitsStats, 3)); // true\n * ```\n */\nexport function isValidArabicContent(charStats: CharacterStats, textLength: number): boolean {\n // Arabic text with reasonable content length is likely valid\n if (charStats.arabicCount >= 3) {\n return true;\n }\n\n // Short Arabic snippets with numbers might be valid (like dates, references)\n if (charStats.arabicCount >= 1 && charStats.digitCount > 0 && textLength <= 20) {\n return true;\n }\n\n // Allow short Arabic words with punctuation (like \"ูู.\" - \"for him/it.\")\n if (charStats.arabicCount >= 2 && charStats.punctuationCount <= 2 && textLength <= 10) {\n return true;\n }\n\n // Allow single meaningful Arabic words that are common standalone terms\n // This handles cases like pronouns, prepositions, common short words\n if (charStats.arabicCount >= 1 && textLength <= 5 && charStats.punctuationCount <= 1) {\n return true;\n }\n\n return false;\n}\n","import type { FixTypoOptions } from './types';\nimport { sanitizeArabic } from './utils/sanitize';\nimport { alignTokenSequences, areSimilarAfterNormalization, calculateSimilarity } from './utils/similarity';\nimport {\n handleFootnoteFusion,\n handleFootnoteSelection,\n handleStandaloneFootnotes,\n tokenizeText,\n} from './utils/textUtils';\n\n/**\n * Selects the best token(s) from an aligned pair during typo correction.\n * Uses various heuristics including normalization, footnote handling, typo symbols,\n * and similarity scores to determine which token(s) to keep.\n *\n * @param originalToken - Token from the original OCR text (may be null)\n * @param altToken - Token from the alternative OCR text (may be null)\n * @param options - Configuration options including typo symbols and similarity threshold\n * @returns Array of selected tokens (usually contains one token, but may contain multiple)\n */\nconst selectBestTokens = (\n originalToken: null | string,\n altToken: null | string,\n { similarityThreshold, typoSymbols }: FixTypoOptions,\n): string[] => {\n // Handle missing tokens\n if (originalToken === null) {\n return [altToken!];\n }\n if (altToken === null) {\n return [originalToken];\n }\n\n // Preserve original if same after normalization (keeps diacritics)\n if (sanitizeArabic(originalToken) === sanitizeArabic(altToken)) {\n return [originalToken];\n }\n\n // Handle embedded footnotes\n const result = handleFootnoteSelection(originalToken, altToken);\n if (result) {\n return result;\n }\n\n // Handle standalone footnotes\n const footnoteResult = handleStandaloneFootnotes(originalToken, altToken);\n if (footnoteResult) {\n return footnoteResult;\n }\n\n // Handle typo symbols - prefer the symbol itself\n if (typoSymbols.includes(originalToken) || typoSymbols.includes(altToken)) {\n const typoSymbol = typoSymbols.find((symbol) => symbol === originalToken || symbol === altToken);\n return typoSymbol ? [typoSymbol] : [originalToken];\n }\n\n // Choose based on similarity\n const normalizedOriginal = sanitizeArabic(originalToken);\n const normalizedAlt = sanitizeArabic(altToken);\n const similarity = calculateSimilarity(normalizedOriginal, normalizedAlt);\n\n return [similarity > similarityThreshold ? originalToken : altToken];\n};\n\n/**\n * Removes duplicate tokens and handles footnote fusion in post-processing.\n * Identifies and removes tokens that are highly similar while preserving\n * important variations. Also handles special cases like footnote merging.\n *\n * @param tokens - Array of tokens to process\n * @param highSimilarityThreshold - Threshold for detecting duplicates (0.0 to 1.0)\n * @returns Array of tokens with duplicates removed and footnotes fused\n */\nconst removeDuplicateTokens = (tokens: string[], highSimilarityThreshold: number): string[] => {\n if (tokens.length === 0) {\n return tokens;\n }\n\n const result: string[] = [];\n\n for (const currentToken of tokens) {\n if (result.length === 0) {\n result.push(currentToken);\n continue;\n }\n\n const previousToken = result.at(-1)!;\n\n // Handle ordinary echoes (similar tokens)\n if (areSimilarAfterNormalization(previousToken, currentToken, highSimilarityThreshold)) {\n // Keep the shorter version\n if (currentToken.length < previousToken.length) {\n result[result.length - 1] = currentToken;\n }\n continue;\n }\n\n // Handle footnote fusion cases\n if (handleFootnoteFusion(result, previousToken, currentToken)) {\n continue;\n }\n\n result.push(currentToken);\n }\n\n return result;\n};\n\n/**\n * Processes text alignment between original and alternate OCR results to fix typos.\n * Uses the Needleman-Wunsch sequence alignment algorithm to align tokens,\n * then selects the best tokens and performs post-processing.\n *\n * @param originalText - Original OCR text that may contain typos\n * @param altText - Reference text from alternate OCR for comparison\n * @param options - Configuration options for alignment and selection\n * @returns Corrected text with typos fixed\n */\nexport const processTextAlignment = (originalText: string, altText: string, options: FixTypoOptions): string => {\n const originalTokens = tokenizeText(originalText, options.typoSymbols);\n const altTokens = tokenizeText(altText, options.typoSymbols);\n\n // Align token sequences\n const alignedPairs = alignTokenSequences(\n originalTokens,\n altTokens,\n options.typoSymbols,\n options.similarityThreshold,\n );\n\n // Select best tokens from each aligned pair\n const mergedTokens = alignedPairs.flatMap(([original, alt]) => selectBestTokens(original, alt, options));\n\n // Remove duplicates and handle post-processing\n const finalTokens = removeDuplicateTokens(mergedTokens, options.highSimilarityThreshold);\n\n return finalTokens.join(' ');\n};\n\n/**\n * Convenience wrapper around {@link processTextAlignment} that accepts partial options.\n *\n * @param original - The source text that may contain typographical errors.\n * @param correction - The reference text used to correct the {@link original} text.\n * @param options - Partial typo correction options combined with required typo symbols.\n * @returns The corrected text generated from the alignment process.\n */\nexport const fixTypo = (\n original: string,\n correction: string,\n {\n highSimilarityThreshold = 0.8,\n similarityThreshold = 0.6,\n typoSymbols,\n }: Partial<FixTypoOptions> & Pick<FixTypoOptions, 'typoSymbols'>,\n) => {\n return processTextAlignment(original, correction, { highSimilarityThreshold, similarityThreshold, typoSymbols });\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["outCode","isBalanced"],"sources":["../src/utils/sanitize.ts","../src/utils/levenshthein.ts","../src/utils/similarity.ts","../src/alignment.ts","../src/balance.ts","../src/utils/textUtils.ts","../src/footnotes.ts","../src/utils/ahocorasick.ts","../src/utils/constants.ts","../src/utils/fuzzyUtils.ts","../src/utils/qgram.ts","../src/fuzzy.ts","../src/noise.ts","../src/typos.ts"],"sourcesContent":["/**\n * Ultra-fast Arabic text sanitizer for search/indexing/display.\n * Optimized for very high call rates: avoids per-call object spreads and minimizes allocations.\n * Options can merge over a base preset or `'none'` to apply exactly the rules you request.\n */\nexport type SanitizePreset = 'light' | 'search' | 'aggressive';\nexport type SanitizeBase = 'none' | SanitizePreset;\n\n/**\n * Public options for {@link sanitizeArabic}. When you pass an options object, it overlays the chosen\n * `base` (default `'light'`) without allocating merged objects on the hot path; flags are resolved\n * directly into local booleans for speed.\n */\nexport type SanitizeOptions = {\n /** Base to merge over. `'none'` applies only the options you specify. Default when passing an object: `'light'`. */\n base?: SanitizeBase;\n\n /**\n * NFC normalization (fast-path).\n *\n * For performance, this sanitizer avoids calling `String.prototype.normalize('NFC')` and instead\n * applies the key Arabic canonical compositions inline (hamza/madda combining marks).\n * This preserves the NFC behavior that matters for typical Arabic OCR text while keeping throughput high.\n *\n * Default: `true` in all presets.\n */\n nfc?: boolean;\n\n /** Strip zero-width controls (U+200BโU+200F, U+202AโU+202E, U+2060โU+2064, U+FEFF). Default: `true` in presets. */\n stripZeroWidth?: boolean;\n\n /** If stripping zero-width, replace them with a space instead of removing. Default: `false`. */\n zeroWidthToSpace?: boolean;\n\n /** Remove Arabic diacritics (tashkฤซl). Default: `true` in `'search'`/`'aggressive'`. */\n stripDiacritics?: boolean;\n\n /** Remove footnote references. Default: `true` in `'search'`/`'aggressive'`. */\n stripFootnotes?: boolean;\n\n /**\n * Remove tatweel (ู).\n * - `true` is treated as `'safe'` (preserves tatweel after digits or 'ู' for dates/list markers)\n * - `'safe'` or `'all'` explicitly\n * - `false` to keep tatweel\n * Default: `'all'` in `'search'`/`'aggressive'`, `false` in `'light'`.\n */\n stripTatweel?: boolean | 'safe' | 'all';\n\n /** Normalize ุข/ุฃ/ุฅ โ ุง. Default: `true` in `'search'`/`'aggressive'`. */\n normalizeAlif?: boolean;\n\n /** Replace ู โ ู. Default: `true` in `'search'`/`'aggressive'`. */\n replaceAlifMaqsurah?: boolean;\n\n /** Replace ุฉ โ ู (lossy). Default: `true` in `'aggressive'` only. */\n replaceTaMarbutahWithHa?: boolean;\n\n /** Strip Latin letters/digits and common OCR noise into spaces. Default: `true` in `'aggressive'`. */\n stripLatinAndSymbols?: boolean;\n\n /** Keep only Arabic letters (no whitespace). Use for compact keys, not FTS. */\n keepOnlyArabicLetters?: boolean;\n\n /** Keep Arabic letters + spaces (drops digits/punct/symbols). Great for FTS. Default: `true` in `'aggressive'`. */\n lettersAndSpacesOnly?: boolean;\n\n /** Collapse runs of whitespace to a single space. Default: `true`. */\n collapseWhitespace?: boolean;\n\n /** Trim leading/trailing whitespace. Default: `true`. */\n trim?: boolean;\n\n /**\n * Remove the Hijri date marker (\"ูู\" or bare \"ู\" if tatweel already removed) when it follows a date-like token\n * (digits/slashes/hyphens/spaces). Example: `1435/3/29 ูู` โ `1435/3/29`.\n * Default: `true` in `'search'`/`'aggressive'`, `false` in `'light'`.\n */\n removeHijriMarker?: boolean;\n};\n\n/** Fully-resolved internal preset options (no `base`, and tatweel as a mode). */\ntype PresetOptions = {\n nfc: boolean;\n stripZeroWidth: boolean;\n zeroWidthToSpace: boolean;\n stripDiacritics: boolean;\n stripFootnotes: boolean;\n stripTatweel: false | 'safe' | 'all';\n normalizeAlif: boolean;\n replaceAlifMaqsurah: boolean;\n replaceTaMarbutahWithHa: boolean;\n stripLatinAndSymbols: boolean;\n keepOnlyArabicLetters: boolean;\n lettersAndSpacesOnly: boolean;\n collapseWhitespace: boolean;\n trim: boolean;\n removeHijriMarker: boolean;\n};\n\n/** Fully-resolved internal options with short names for performance. */\ntype ResolvedOptions = {\n nfc: boolean;\n stripZW: boolean;\n zwAsSpace: boolean;\n removeHijri: boolean;\n removeDia: boolean;\n tatweelMode: false | 'safe' | 'all';\n normAlif: boolean;\n maqToYa: boolean;\n taToHa: boolean;\n removeFootnotes: boolean;\n lettersSpacesOnly: boolean;\n stripNoise: boolean;\n lettersOnly: boolean;\n collapseWS: boolean;\n doTrim: boolean;\n};\n\nconst PRESETS: Record<SanitizePreset, PresetOptions> = {\n aggressive: {\n collapseWhitespace: true,\n keepOnlyArabicLetters: false,\n lettersAndSpacesOnly: true,\n nfc: true,\n normalizeAlif: true,\n removeHijriMarker: true,\n replaceAlifMaqsurah: true,\n replaceTaMarbutahWithHa: true,\n stripDiacritics: true,\n stripFootnotes: true,\n stripLatinAndSymbols: true,\n stripTatweel: 'all',\n stripZeroWidth: true,\n trim: true,\n zeroWidthToSpace: false,\n },\n light: {\n collapseWhitespace: true,\n keepOnlyArabicLetters: false,\n lettersAndSpacesOnly: false,\n nfc: true,\n normalizeAlif: false,\n removeHijriMarker: false,\n replaceAlifMaqsurah: false,\n replaceTaMarbutahWithHa: false,\n stripDiacritics: false,\n stripFootnotes: false,\n stripLatinAndSymbols: false,\n stripTatweel: false,\n stripZeroWidth: true,\n trim: true,\n zeroWidthToSpace: false,\n },\n search: {\n collapseWhitespace: true,\n keepOnlyArabicLetters: false,\n lettersAndSpacesOnly: false,\n nfc: true,\n normalizeAlif: true,\n removeHijriMarker: true,\n replaceAlifMaqsurah: true,\n replaceTaMarbutahWithHa: false,\n stripDiacritics: true,\n stripFootnotes: true,\n stripLatinAndSymbols: false,\n stripTatweel: 'all',\n stripZeroWidth: true,\n trim: true,\n zeroWidthToSpace: false,\n },\n} as const;\n\nconst PRESET_NONE: PresetOptions = {\n collapseWhitespace: false,\n keepOnlyArabicLetters: false,\n lettersAndSpacesOnly: false,\n nfc: false,\n normalizeAlif: false,\n removeHijriMarker: false,\n replaceAlifMaqsurah: false,\n replaceTaMarbutahWithHa: false,\n stripDiacritics: false,\n stripFootnotes: false,\n stripLatinAndSymbols: false,\n stripTatweel: false,\n stripZeroWidth: false,\n trim: false,\n zeroWidthToSpace: false,\n};\n\n// Constants for character codes\nconst CHAR_SPACE = 32;\nconst CHAR_TATWEEL = 0x0640;\nconst CHAR_HA = 0x0647;\nconst CHAR_YA = 0x064a;\nconst CHAR_WAW = 0x0648;\nconst CHAR_ALIF = 0x0627;\nconst CHAR_ALIF_MADDA = 0x0622;\nconst CHAR_ALIF_HAMZA_ABOVE = 0x0623;\nconst CHAR_WAW_HAMZA_ABOVE = 0x0624;\nconst CHAR_ALIF_HAMZA_BELOW = 0x0625;\nconst CHAR_YEH_HAMZA_ABOVE = 0x0626;\nconst CHAR_ALIF_WASLA = 0x0671;\nconst CHAR_ALIF_MAQSURAH = 0x0649;\nconst CHAR_TA_MARBUTAH = 0x0629;\nconst CHAR_MADDA_ABOVE = 0x0653;\nconst CHAR_HAMZA_ABOVE_MARK = 0x0654;\nconst CHAR_HAMZA_BELOW_MARK = 0x0655;\n\n// Shared resources to avoid allocations\nlet sharedBuffer = new Uint16Array(2048); // Start with 2KB (enough for ~1000 chars)\nconst decoder = new TextDecoder('utf-16le');\n\n// Diacritic ranges\nconst isDiacritic = (code: number): boolean => {\n return (\n (code >= 0x064b && code <= 0x065f) ||\n (code >= 0x0610 && code <= 0x061a) ||\n code === 0x0670 ||\n (code >= 0x06d6 && code <= 0x06ed)\n );\n};\n\nconst isZeroWidth = (code: number): boolean => {\n return (\n (code >= 0x200b && code <= 0x200f) ||\n (code >= 0x202a && code <= 0x202e) ||\n (code >= 0x2060 && code <= 0x2064) ||\n code === 0xfeff\n );\n};\n\nconst isLatinOrDigit = (code: number): boolean => {\n return (\n (code >= 65 && code <= 90) || // A-Z\n (code >= 97 && code <= 122) || // a-z\n (code >= 48 && code <= 57) // 0-9\n );\n};\n\nconst isSymbol = (code: number): boolean => {\n // [ยฌยง`=]|[&]|[๏ทบ]\n return (\n code === 0x00ac || // ยฌ\n code === 0x00a7 || // ยง\n code === 0x0060 || // `\n code === 0x003d || // =\n code === 0x0026 || // &\n code === 0xfdfa // ๏ทบ\n );\n};\n\nconst isArabicLetter = (code: number): boolean => {\n return (\n (code >= 0x0621 && code <= 0x063a) ||\n (code >= 0x0641 && code <= 0x064a) ||\n code === 0x0671 ||\n code === 0x067e ||\n code === 0x0686 ||\n (code >= 0x06a4 && code <= 0x06af) ||\n code === 0x06cc ||\n code === 0x06d2 ||\n code === 0x06d3\n );\n};\n\n/**\n * Checks whether a code point represents a Western or Arabic-Indic digit.\n *\n * @param code - The numeric code point to evaluate.\n * @returns True when the code point is a digit in either numeral system.\n */\nconst isDigit = (code: number): boolean => (code >= 48 && code <= 57) || (code >= 0x0660 && code <= 0x0669);\n\n/**\n * Resolves a boolean by taking an optional override over a preset value.\n *\n * @param presetValue - The value defined by the preset.\n * @param override - Optional override provided by the caller.\n * @returns The resolved boolean value.\n */\nconst resolveBoolean = (presetValue: boolean, override?: boolean): boolean =>\n override === undefined ? presetValue : !!override;\n\n/**\n * Resolves the tatweel mode by taking an optional override over a preset mode.\n * An override of `true` maps to `'safe'` for convenience.\n *\n * @param presetValue - The mode specified by the preset.\n * @param override - Optional override provided by the caller.\n * @returns The resolved tatweel mode.\n */\nconst resolveTatweelMode = (\n presetValue: false | 'safe' | 'all',\n override?: boolean | 'safe' | 'all',\n): false | 'safe' | 'all' => {\n if (override === undefined) {\n return presetValue;\n }\n if (override === true) {\n return 'safe';\n }\n if (override === false) {\n return false;\n }\n return override;\n};\n\n/**\n * Internal sanitization logic that applies all transformations to a single string.\n * Uses single-pass character transformation for maximum performance when possible.\n * This function assumes all options have been pre-resolved for maximum performance.\n */\nconst applySanitization = (input: string, options: ResolvedOptions): string => {\n if (!input) {\n return '';\n }\n\n const {\n nfc,\n stripZW,\n zwAsSpace,\n removeHijri,\n removeDia,\n tatweelMode,\n normAlif,\n maqToYa,\n taToHa,\n removeFootnotes,\n lettersSpacesOnly,\n stripNoise,\n lettersOnly,\n collapseWS,\n doTrim,\n } = options;\n\n /**\n * NFC Normalization (Fast Path)\n *\n * `String.prototype.normalize('NFC')` is extremely expensive under high throughput.\n * For Arabic OCR text, the main canonical compositions we care about are:\n * - ุง + โู (U+0653) โ ุข\n * - ุง + โู (U+0654) โ ุฃ\n * - ุง + โู (U+0655) โ ุฅ\n * - ู + โู (U+0654) โ ุค\n * - ู + โู (U+0654) โ ุฆ\n *\n * We implement these compositions inline during the main loop, avoiding full NFC\n * normalization in the common case while preserving behavior needed by our sanitizer.\n */\n const text = input;\n const len = text.length;\n\n // Ensure shared buffer is large enough\n if (len > sharedBuffer.length) {\n sharedBuffer = new Uint16Array(len + 1024);\n }\n const buffer = sharedBuffer;\n let bufIdx = 0;\n\n let lastWasSpace = false;\n\n // Skip leading whitespace if trimming\n let start = 0;\n if (doTrim) {\n while (start < len && text.charCodeAt(start) <= 32) {\n start++;\n }\n }\n\n for (let i = start; i < len; i++) {\n const code = text.charCodeAt(i);\n\n // Whitespace handling\n if (code <= 32) {\n if (lettersOnly) {\n continue; // Drop spaces if lettersOnly\n }\n\n if (collapseWS) {\n if (!lastWasSpace && bufIdx > 0) {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = true;\n }\n } else {\n buffer[bufIdx++] = code; // Keep original whitespace\n lastWasSpace = false;\n }\n continue;\n }\n\n // NFC (subset) for Arabic canonical compositions: merge combining marks into previous output\n if (nfc) {\n if (code === CHAR_MADDA_ABOVE || code === CHAR_HAMZA_ABOVE_MARK || code === CHAR_HAMZA_BELOW_MARK) {\n const prevIdx = bufIdx - 1;\n if (prevIdx >= 0) {\n const prev = buffer[prevIdx];\n let composed = 0;\n\n if (prev === CHAR_ALIF) {\n if (code === CHAR_MADDA_ABOVE) {\n composed = CHAR_ALIF_MADDA;\n } else if (code === CHAR_HAMZA_ABOVE_MARK) {\n composed = CHAR_ALIF_HAMZA_ABOVE;\n } else {\n // CHAR_HAMZA_BELOW_MARK\n composed = CHAR_ALIF_HAMZA_BELOW;\n }\n } else if (code === CHAR_HAMZA_ABOVE_MARK) {\n // Only Hamza Above composes for WAW/YEH in NFC\n if (prev === CHAR_WAW) {\n composed = CHAR_WAW_HAMZA_ABOVE;\n } else if (prev === CHAR_YA) {\n composed = CHAR_YEH_HAMZA_ABOVE;\n }\n }\n\n if (composed !== 0) {\n buffer[prevIdx] = composed;\n continue;\n }\n }\n }\n }\n\n // Zero width\n if (stripZW && isZeroWidth(code)) {\n if (zwAsSpace) {\n if (collapseWS) {\n if (!lastWasSpace && bufIdx > 0) {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = true;\n }\n } else {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = false;\n }\n }\n continue;\n }\n\n // Hijri Marker Removal (Must run before letter filtering removes digits)\n if (removeHijri && code === CHAR_HA) {\n let nextIdx = i + 1;\n if (nextIdx < len && text.charCodeAt(nextIdx) === CHAR_TATWEEL) {\n nextIdx++;\n }\n\n let isBoundary = false;\n if (nextIdx >= len) {\n isBoundary = true;\n } else {\n const nextCode = text.charCodeAt(nextIdx);\n if (nextCode <= 32 || isSymbol(nextCode) || nextCode === 47 || nextCode === 45) {\n isBoundary = true;\n }\n }\n\n if (isBoundary) {\n let backIdx = i - 1;\n while (backIdx >= 0) {\n const c = text.charCodeAt(backIdx);\n if (c <= 32 || isZeroWidth(c)) {\n backIdx--;\n } else {\n break;\n }\n }\n if (backIdx >= 0 && isDigit(text.charCodeAt(backIdx))) {\n if (nextIdx > i + 1) {\n i++;\n }\n continue;\n }\n }\n }\n\n // Diacritics\n if (removeDia && isDiacritic(code)) {\n continue;\n }\n\n // Tatweel\n if (code === CHAR_TATWEEL) {\n if (tatweelMode === 'all') {\n continue;\n }\n if (tatweelMode === 'safe') {\n let backIdx = bufIdx - 1;\n while (backIdx >= 0 && buffer[backIdx] === CHAR_SPACE) {\n backIdx--;\n }\n if (backIdx >= 0) {\n const prev = buffer[backIdx];\n if (isDigit(prev) || prev === CHAR_HA) {\n // Keep it\n } else {\n continue; // Drop\n }\n } else {\n continue; // Drop\n }\n }\n }\n\n // Latin and Symbols (Skip if letter filtering will handle it)\n if (stripNoise && !lettersSpacesOnly && !lettersOnly) {\n if (isLatinOrDigit(code) || isSymbol(code)) {\n if (collapseWS) {\n if (!lastWasSpace && bufIdx > 0) {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = true;\n }\n } else {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = false;\n }\n continue;\n }\n // Double slash check //\n if (code === 47 && i + 1 < len && text.charCodeAt(i + 1) === 47) {\n while (i + 1 < len && text.charCodeAt(i + 1) === 47) {\n i++;\n }\n if (collapseWS) {\n if (!lastWasSpace && bufIdx > 0) {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = true;\n }\n } else {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = false;\n }\n continue;\n }\n }\n\n // Footnote Removal (Skip if letter filtering will handle it)\n if (removeFootnotes && !lettersSpacesOnly && !lettersOnly && code === 40) {\n // (\n let nextIdx = i + 1;\n if (nextIdx < len && text.charCodeAt(nextIdx) === CHAR_SPACE) {\n nextIdx++;\n }\n\n if (nextIdx < len) {\n const c1 = text.charCodeAt(nextIdx);\n\n // Pattern 1: (ยฌ123...)\n if (c1 === 0x00ac) {\n // ยฌ\n nextIdx++;\n let hasDigits = false;\n while (nextIdx < len) {\n const c = text.charCodeAt(nextIdx);\n if (c >= 0x0660 && c <= 0x0669) {\n hasDigits = true;\n nextIdx++;\n } else {\n break;\n }\n }\n if (hasDigits && nextIdx < len) {\n if (text.charCodeAt(nextIdx) === 41) {\n // )\n i = nextIdx;\n if (collapseWS) {\n if (!lastWasSpace && bufIdx > 0) {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = true;\n }\n } else {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = false;\n }\n continue;\n }\n if (text.charCodeAt(nextIdx) === CHAR_SPACE) {\n nextIdx++;\n if (nextIdx < len && text.charCodeAt(nextIdx) === 41) {\n i = nextIdx;\n if (collapseWS) {\n if (!lastWasSpace && bufIdx > 0) {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = true;\n }\n } else {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = false;\n }\n continue;\n }\n }\n }\n }\n\n // Pattern 2: (1) or (1 X)\n else if (c1 >= 0x0660 && c1 <= 0x0669) {\n let tempIdx = nextIdx + 1;\n let matched = false;\n\n if (tempIdx < len) {\n const c2 = text.charCodeAt(tempIdx);\n if (c2 === 41) {\n // )\n matched = true;\n tempIdx++;\n } else if (c2 === CHAR_SPACE) {\n // Space\n tempIdx++;\n if (tempIdx < len) {\n const c3 = text.charCodeAt(tempIdx);\n if (c3 >= 0x0600 && c3 <= 0x06ff) {\n tempIdx++;\n if (tempIdx < len && text.charCodeAt(tempIdx) === 41) {\n matched = true;\n tempIdx++;\n }\n }\n }\n }\n }\n\n if (matched) {\n i = tempIdx - 1;\n if (collapseWS) {\n if (!lastWasSpace && bufIdx > 0) {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = true;\n }\n } else {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = false;\n }\n continue;\n }\n }\n }\n }\n\n // Letter Filtering (Aggressive)\n if (lettersSpacesOnly || lettersOnly) {\n if (!isArabicLetter(code)) {\n if (lettersOnly) {\n continue;\n }\n // lettersSpacesOnly -> replace with space\n if (collapseWS) {\n if (!lastWasSpace && bufIdx > 0) {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = true;\n }\n } else {\n buffer[bufIdx++] = CHAR_SPACE;\n lastWasSpace = false;\n }\n continue;\n }\n\n // Normalization logic duplicated for speed\n let outCode = code;\n if (normAlif) {\n if (\n code === CHAR_ALIF_MADDA ||\n code === CHAR_ALIF_HAMZA_ABOVE ||\n code === CHAR_ALIF_HAMZA_BELOW ||\n code === CHAR_ALIF_WASLA\n ) {\n outCode = CHAR_ALIF;\n }\n }\n if (maqToYa && code === CHAR_ALIF_MAQSURAH) {\n outCode = CHAR_YA;\n }\n if (taToHa && code === CHAR_TA_MARBUTAH) {\n outCode = CHAR_HA;\n }\n\n buffer[bufIdx++] = outCode;\n lastWasSpace = false;\n continue;\n }\n\n // Normalization\n let outCode = code;\n if (normAlif) {\n if (\n code === CHAR_ALIF_MADDA ||\n code === CHAR_ALIF_HAMZA_ABOVE ||\n code === CHAR_ALIF_HAMZA_BELOW ||\n code === CHAR_ALIF_WASLA\n ) {\n outCode = CHAR_ALIF;\n }\n }\n if (maqToYa && code === CHAR_ALIF_MAQSURAH) {\n outCode = CHAR_YA;\n }\n if (taToHa && code === CHAR_TA_MARBUTAH) {\n outCode = CHAR_HA;\n }\n\n buffer[bufIdx++] = outCode;\n lastWasSpace = false;\n }\n\n // Trailing trim\n if (doTrim && lastWasSpace && bufIdx > 0) {\n bufIdx--;\n }\n\n if (bufIdx === 0) {\n return '';\n }\n const resultView = buffer.subarray(0, bufIdx);\n return decoder.decode(resultView);\n};\n\n/**\n * Resolves options from a preset or custom options object.\n * Returns all resolved flags for reuse in batch processing.\n */\nconst resolveOptions = (optionsOrPreset: SanitizePreset | SanitizeOptions): ResolvedOptions => {\n let preset: PresetOptions;\n let opts: SanitizeOptions | null = null;\n\n if (typeof optionsOrPreset === 'string') {\n preset = PRESETS[optionsOrPreset];\n } else {\n const base = optionsOrPreset.base ?? 'light';\n preset = base === 'none' ? PRESET_NONE : PRESETS[base];\n opts = optionsOrPreset;\n }\n\n return {\n collapseWS: resolveBoolean(preset.collapseWhitespace, opts?.collapseWhitespace),\n doTrim: resolveBoolean(preset.trim, opts?.trim),\n lettersOnly: resolveBoolean(preset.keepOnlyArabicLetters, opts?.keepOnlyArabicLetters),\n lettersSpacesOnly: resolveBoolean(preset.lettersAndSpacesOnly, opts?.lettersAndSpacesOnly),\n maqToYa: resolveBoolean(preset.replaceAlifMaqsurah, opts?.replaceAlifMaqsurah),\n nfc: resolveBoolean(preset.nfc, opts?.nfc),\n normAlif: resolveBoolean(preset.normalizeAlif, opts?.normalizeAlif),\n removeDia: resolveBoolean(preset.stripDiacritics, opts?.stripDiacritics),\n removeFootnotes: resolveBoolean(preset.stripFootnotes, opts?.stripFootnotes),\n removeHijri: resolveBoolean(preset.removeHijriMarker, opts?.removeHijriMarker),\n stripNoise: resolveBoolean(preset.stripLatinAndSymbols, opts?.stripLatinAndSymbols),\n stripZW: resolveBoolean(preset.stripZeroWidth, opts?.stripZeroWidth),\n taToHa: resolveBoolean(preset.replaceTaMarbutahWithHa, opts?.replaceTaMarbutahWithHa),\n tatweelMode: resolveTatweelMode(preset.stripTatweel, opts?.stripTatweel),\n zwAsSpace: resolveBoolean(preset.zeroWidthToSpace, opts?.zeroWidthToSpace),\n };\n};\n\n/**\n * Creates a reusable sanitizer function with pre-resolved options.\n * Use this when you need to sanitize many strings with the same options\n * for maximum performance.\n *\n * @example\n * ```ts\n * const sanitize = createArabicSanitizer('search');\n * const results = texts.map(sanitize);\n * ```\n */\nexport const createArabicSanitizer = (\n optionsOrPreset: SanitizePreset | SanitizeOptions = 'search',\n): ((input: string) => string) => {\n const resolved = resolveOptions(optionsOrPreset);\n\n return (input: string): string => applySanitization(input, resolved);\n};\n\n/**\n * Sanitizes Arabic text according to a preset or custom options.\n *\n * Presets:\n * - `'light'`: NFC, zero-width removal, collapse/trim spaces.\n * - `'search'`: removes diacritics and tatweel, normalizes Alif and ูโู, removes Hijri marker.\n * - `'aggressive'`: ideal for FTS; keeps letters+spaces only and strips common noise.\n *\n * Custom options:\n * - Passing an options object overlays the selected `base` preset (default `'light'`).\n * - Use `base: 'none'` to apply **only** the rules you specify (e.g., tatweel only).\n *\n * **Batch processing**: Pass an array of strings for optimized batch processing.\n * Options are resolved once and applied to all strings, providing significant\n * performance gains over calling the function in a loop.\n *\n * Examples:\n * ```ts\n * sanitizeArabic('ุฃุจูููุชููููููุฉู', { base: 'none', stripTatweel: true }); // 'ุฃุจุชูููุฉู'\n * sanitizeArabic('1435/3/29 ูู', 'aggressive'); // '1435 3 29'\n * sanitizeArabic('ุงููุณููููุงู
ู ุนูููููููู
ู', 'search'); // 'ุงูุณูุงู
ุนูููู
'\n *\n * // Batch processing (optimized):\n * sanitizeArabic(['text1', 'text2', 'text3'], 'search'); // ['result1', 'result2', 'result3']\n * ```\n */\nexport function sanitizeArabic(input: string, optionsOrPreset?: SanitizePreset | SanitizeOptions): string;\nexport function sanitizeArabic(input: string[], optionsOrPreset?: SanitizePreset | SanitizeOptions): string[];\nexport function sanitizeArabic(\n input: string | string[],\n optionsOrPreset: SanitizePreset | SanitizeOptions = 'search',\n): string | string[] {\n // Handle array input with optimized batch processing\n if (Array.isArray(input)) {\n if (input.length === 0) {\n return [];\n }\n\n const resolved = resolveOptions(optionsOrPreset);\n\n // Per-string processing using the optimized single-pass sanitizer\n const results: string[] = new Array(input.length);\n\n for (let i = 0; i < input.length; i++) {\n results[i] = applySanitization(input[i], resolved);\n }\n\n return results;\n }\n\n // Single string: resolve options and apply\n if (!input) {\n return '';\n }\n\n const resolved = resolveOptions(optionsOrPreset);\n\n return applySanitization(input, resolved);\n}\n","/**\n * Calculates Levenshtein distance between two strings using space-optimized dynamic programming.\n * The Levenshtein distance is the minimum number of single-character edits (insertions,\n * deletions, or substitutions) required to change one string into another.\n *\n * @param textA - First string to compare\n * @param textB - Second string to compare\n * @returns Minimum edit distance between the two strings\n * @complexity Time: O(m*n), Space: O(min(m,n)) where m,n are string lengths\n * @example\n * calculateLevenshteinDistance('kitten', 'sitting') // Returns 3\n * calculateLevenshteinDistance('', 'hello') // Returns 5\n */\nexport const calculateLevenshteinDistance = (textA: string, textB: string): number => {\n const lengthA = textA.length;\n const lengthB = textB.length;\n\n if (lengthA === 0) {\n return lengthB;\n }\n\n if (lengthB === 0) {\n return lengthA;\n }\n\n // Use shorter string for the array to optimize space\n const [shorter, longer] = lengthA <= lengthB ? [textA, textB] : [textB, textA];\n const shortLen = shorter.length;\n const longLen = longer.length;\n\n let previousRow = Array.from({ length: shortLen + 1 }, (_, index) => index);\n\n for (let i = 1; i <= longLen; i++) {\n const currentRow = [i];\n\n for (let j = 1; j <= shortLen; j++) {\n const substitutionCost = longer[i - 1] === shorter[j - 1] ? 0 : 1;\n const minCost = Math.min(\n previousRow[j] + 1, // deletion\n currentRow[j - 1] + 1, // insertion\n previousRow[j - 1] + substitutionCost, // substitution\n );\n currentRow.push(minCost);\n }\n\n previousRow = currentRow;\n }\n\n return previousRow[shortLen];\n};\n\n/**\n * Early exit check for bounded Levenshtein distance.\n */\nconst shouldEarlyExit = (a: string, b: string, maxDist: number): number | null => {\n if (Math.abs(a.length - b.length) > maxDist) {\n return maxDist + 1;\n }\n if (a.length === 0) {\n return b.length <= maxDist ? b.length : maxDist + 1;\n }\n if (b.length === 0) {\n return a.length <= maxDist ? a.length : maxDist + 1;\n }\n return null;\n};\n\n/**\n * Initializes arrays for bounded Levenshtein calculation.\n */\nconst initializeBoundedArrays = (m: number): [Int16Array, Int16Array] => {\n const prev = new Int16Array(m + 1);\n const curr = new Int16Array(m + 1);\n for (let j = 0; j <= m; j++) {\n prev[j] = j;\n }\n return [prev, curr];\n};\n\n/**\n * Calculates the bounds for the current row in bounded Levenshtein.\n */\nconst getRowBounds = (i: number, maxDist: number, m: number) => ({\n from: Math.max(1, i - maxDist),\n to: Math.min(m, i + maxDist),\n});\n\n/**\n * Processes a single cell in the bounded Levenshtein matrix.\n */\nconst processBoundedCell = (a: string, b: string, i: number, j: number, prev: Int16Array, curr: Int16Array): number => {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1;\n const del = prev[j] + 1;\n const ins = curr[j - 1] + 1;\n const sub = prev[j - 1] + cost;\n return Math.min(del, ins, sub);\n};\n\n/**\n * Processes a single row in bounded Levenshtein calculation.\n */\nconst processBoundedRow = (\n a: string,\n b: string,\n i: number,\n maxDist: number,\n prev: Int16Array,\n curr: Int16Array,\n): number => {\n const m = b.length;\n const big = maxDist + 1;\n const { from, to } = getRowBounds(i, maxDist, m);\n\n curr[0] = i;\n let rowMin = i;\n\n // Fill out-of-bounds cells\n for (let j = 1; j < from; j++) {\n curr[j] = big;\n }\n for (let j = to + 1; j <= m; j++) {\n curr[j] = big;\n }\n\n // Process valid range\n for (let j = from; j <= to; j++) {\n const val = processBoundedCell(a, b, i, j, prev, curr);\n curr[j] = val;\n if (val < rowMin) {\n rowMin = val;\n }\n }\n\n return rowMin;\n};\n\n/**\n * Calculates bounded Levenshtein distance with early termination.\n * More efficient when you only care about distances up to a threshold.\n */\nexport const boundedLevenshtein = (a: string, b: string, maxDist: number): number => {\n const big = maxDist + 1;\n\n // Early exit checks\n const earlyResult = shouldEarlyExit(a, b, maxDist);\n if (earlyResult !== null) {\n return earlyResult;\n }\n\n // Ensure a is shorter for optimization\n if (a.length > b.length) {\n return boundedLevenshtein(b, a, maxDist);\n }\n\n // use `let` so we can swap references instead of copying contents\n let [prev, curr] = initializeBoundedArrays(b.length);\n\n for (let i = 1; i <= a.length; i++) {\n const rowMin = processBoundedRow(a, b, i, maxDist, prev, curr);\n if (rowMin > maxDist) {\n return big;\n }\n\n // O(1) swap instead of O(m) copy\n const tmp = prev;\n prev = curr;\n curr = tmp;\n }\n\n return prev[b.length] <= maxDist ? prev[b.length] : big;\n};\n","import { calculateLevenshteinDistance } from './levenshthein';\nimport { sanitizeArabic } from './sanitize';\n\n// Alignment scoring constants\nconst ALIGNMENT_SCORES = {\n GAP_PENALTY: -1,\n MISMATCH_PENALTY: -2,\n PERFECT_MATCH: 2,\n SOFT_MATCH: 1,\n} as const;\n\n/**\n * Calculates similarity ratio between two strings as a value between 0.0 and 1.0.\n * Uses Levenshtein distance normalized by the length of the longer string.\n * A ratio of 1.0 indicates identical strings, 0.0 indicates completely different strings.\n *\n * @param textA - First string to compare\n * @param textB - Second string to compare\n * @returns Similarity ratio from 0.0 (completely different) to 1.0 (identical)\n * @example\n * calculateSimilarity('hello', 'hello') // Returns 1.0\n * calculateSimilarity('hello', 'help') // Returns 0.6\n */\nexport const calculateSimilarity = (textA: string, textB: string): number => {\n const maxLength = Math.max(textA.length, textB.length) || 1;\n const distance = calculateLevenshteinDistance(textA, textB);\n return (maxLength - distance) / maxLength;\n};\n\n/**\n * Checks if two texts are similar after Arabic normalization.\n * Normalizes both texts by removing diacritics and decorative elements,\n * then compares their similarity against the provided threshold.\n *\n * @param textA - First text to compare\n * @param textB - Second text to compare\n * @param threshold - Similarity threshold (0.0 to 1.0)\n * @returns True if normalized texts meet the similarity threshold\n * @example\n * areSimilarAfterNormalization('ุงูุณูููุงู
', 'ุงูุณูุงู
', 0.9) // Returns true\n */\nexport const areSimilarAfterNormalization = (textA: string, textB: string, threshold: number = 0.6): boolean => {\n const normalizedA = sanitizeArabic(textA);\n const normalizedB = sanitizeArabic(textB);\n return calculateSimilarity(normalizedA, normalizedB) >= threshold;\n};\n\n/**\n * Calculates alignment score for two tokens in sequence alignment.\n * Uses different scoring criteria: perfect match after normalization gets highest score,\n * typo symbols or highly similar tokens get soft match score, mismatches get penalty.\n *\n * @param tokenA - First token to score\n * @param tokenB - Second token to score\n * @param typoSymbols - Array of special symbols that get preferential treatment\n * @param similarityThreshold - Threshold for considering tokens highly similar\n * @returns Alignment score (higher is better match)\n * @example\n * calculateAlignmentScore('hello', 'hello', [], 0.8) // Returns 2 (perfect match)\n * calculateAlignmentScore('hello', 'help', [], 0.8) // Returns 1 or -2 based on similarity\n */\nexport const calculateAlignmentScore = (\n tokenA: string,\n tokenB: string,\n typoSymbols: string[],\n similarityThreshold: number,\n): number => {\n const normalizedA = sanitizeArabic(tokenA);\n const normalizedB = sanitizeArabic(tokenB);\n\n if (normalizedA === normalizedB) {\n return ALIGNMENT_SCORES.PERFECT_MATCH;\n }\n\n const isTypoSymbol = typoSymbols.includes(tokenA) || typoSymbols.includes(tokenB);\n const isHighlySimilar = calculateSimilarity(normalizedA, normalizedB) >= similarityThreshold;\n\n return isTypoSymbol || isHighlySimilar ? ALIGNMENT_SCORES.SOFT_MATCH : ALIGNMENT_SCORES.MISMATCH_PENALTY;\n};\n\ntype AlignedTokenPair = [null | string, null | string];\n\ntype AlignmentCell = {\n direction: 'diagonal' | 'left' | 'up' | null;\n score: number;\n};\n\n/**\n * Backtracks through the scoring matrix to reconstruct optimal sequence alignment.\n * Follows the directional indicators in the matrix to build the sequence of aligned\n * token pairs from the Needleman-Wunsch algorithm.\n *\n * @param matrix - Scoring matrix with directional information from alignment\n * @param tokensA - First sequence of tokens\n * @param tokensB - Second sequence of tokens\n * @returns Array of aligned token pairs, where null indicates a gap\n * @throws Error if invalid alignment direction is encountered\n */\nexport const backtrackAlignment = (\n matrix: AlignmentCell[][],\n tokensA: string[],\n tokensB: string[],\n): AlignedTokenPair[] => {\n const alignment: AlignedTokenPair[] = [];\n let i = tokensA.length;\n let j = tokensB.length;\n\n while (i > 0 || j > 0) {\n const currentCell = matrix[i][j];\n\n switch (currentCell.direction) {\n case 'diagonal':\n alignment.push([tokensA[--i], tokensB[--j]]);\n break;\n case 'left':\n alignment.push([null, tokensB[--j]]);\n break;\n case 'up':\n alignment.push([tokensA[--i], null]);\n break;\n default:\n throw new Error('Invalid alignment direction');\n }\n }\n\n return alignment.reverse();\n};\n\n/**\n * Initializes the scoring matrix with gap penalties.\n *\n * @param lengthA - Length of the first token sequence.\n * @param lengthB - Length of the second token sequence.\n * @returns A matrix seeded with gap penalties for alignment.\n */\nconst initializeScoringMatrix = (lengthA: number, lengthB: number): AlignmentCell[][] => {\n const matrix: AlignmentCell[][] = Array.from({ length: lengthA + 1 }, () =>\n Array.from({ length: lengthB + 1 }, () => ({ direction: null, score: 0 })),\n );\n\n // Initialize first row and column with gap penalties\n for (let i = 1; i <= lengthA; i++) {\n matrix[i][0] = { direction: 'up', score: i * ALIGNMENT_SCORES.GAP_PENALTY };\n }\n for (let j = 1; j <= lengthB; j++) {\n matrix[0][j] = { direction: 'left', score: j * ALIGNMENT_SCORES.GAP_PENALTY };\n }\n\n return matrix;\n};\n\n/**\n * Determines the best alignment direction and score for a cell.\n *\n * @param diagonalScore - Score achieved by aligning tokens diagonally.\n * @param upScore - Score achieved by inserting a gap in the second sequence.\n * @param leftScore - Score achieved by inserting a gap in the first sequence.\n * @returns The direction and score that maximize the alignment.\n */\nconst getBestAlignment = (\n diagonalScore: number,\n upScore: number,\n leftScore: number,\n): { direction: 'diagonal' | 'up' | 'left'; score: number } => {\n const maxScore = Math.max(diagonalScore, upScore, leftScore);\n\n if (maxScore === diagonalScore) {\n return { direction: 'diagonal', score: maxScore };\n }\n if (maxScore === upScore) {\n return { direction: 'up', score: maxScore };\n }\n return { direction: 'left', score: maxScore };\n};\n\n/**\n * Performs global sequence alignment using the Needleman-Wunsch algorithm.\n * Aligns two token sequences to find the optimal pairing that maximizes\n * the total alignment score, handling insertions, deletions, and substitutions.\n *\n * @param tokensA - First sequence of tokens to align\n * @param tokensB - Second sequence of tokens to align\n * @param typoSymbols - Special symbols that affect scoring\n * @param similarityThreshold - Threshold for high similarity scoring\n * @returns Array of aligned token pairs, with null indicating gaps\n * @example\n * alignTokenSequences(['a', 'b'], ['a', 'c'], [], 0.8)\n * // Returns [['a', 'a'], ['b', 'c']]\n */\nexport const alignTokenSequences = (\n tokensA: string[],\n tokensB: string[],\n typoSymbols: string[],\n similarityThreshold: number,\n): AlignedTokenPair[] => {\n const lengthA = tokensA.length;\n const lengthB = tokensB.length;\n\n const matrix = initializeScoringMatrix(lengthA, lengthB);\n const typoSymbolsSet = new Set(typoSymbols);\n const normalizedA = tokensA.map((t) => sanitizeArabic(t));\n const normalizedB = tokensB.map((t) => sanitizeArabic(t));\n\n // Fill scoring matrix\n for (let i = 1; i <= lengthA; i++) {\n for (let j = 1; j <= lengthB; j++) {\n const aNorm = normalizedA[i - 1];\n const bNorm = normalizedB[j - 1];\n let alignmentScore: number;\n if (aNorm === bNorm) {\n alignmentScore = ALIGNMENT_SCORES.PERFECT_MATCH;\n } else {\n const isTypo = typoSymbolsSet.has(tokensA[i - 1]) || typoSymbolsSet.has(tokensB[j - 1]);\n const highSim = calculateSimilarity(aNorm, bNorm) >= similarityThreshold;\n alignmentScore = isTypo || highSim ? ALIGNMENT_SCORES.SOFT_MATCH : ALIGNMENT_SCORES.MISMATCH_PENALTY;\n }\n\n const diagonalScore = matrix[i - 1][j - 1].score + alignmentScore;\n const upScore = matrix[i - 1][j].score + ALIGNMENT_SCORES.GAP_PENALTY;\n const leftScore = matrix[i][j - 1].score + ALIGNMENT_SCORES.GAP_PENALTY;\n\n const { direction, score } = getBestAlignment(diagonalScore, upScore, leftScore);\n matrix[i][j] = { direction, score };\n }\n }\n\n return backtrackAlignment(matrix, tokensA, tokensB);\n};\n","import { sanitizeArabic } from './utils/sanitize';\nimport { areSimilarAfterNormalization, calculateSimilarity } from './utils/similarity';\n\n/**\n * Aligns split text segments to match target lines by finding the best order.\n *\n * This function handles cases where text lines have been split into segments\n * and need to be merged back together in the correct order. It compares\n * different arrangements of the segments against target lines to find the\n * best match based on similarity scores.\n *\n * @param targetLines - Array where each element is either a string to align against, or falsy to skip alignment\n * @param segmentLines - Array of text segments that may represent split versions of target lines.\n * @returns Array of aligned text lines\n */\nexport const alignTextSegments = (targetLines: string[], segmentLines: string[]) => {\n const alignedLines: string[] = [];\n let segmentIndex = 0;\n\n for (const targetLine of targetLines) {\n if (segmentIndex >= segmentLines.length) {\n break;\n }\n\n if (targetLine) {\n // Process line that needs alignment\n const { result, segmentsConsumed } = processAlignmentTarget(targetLine, segmentLines, segmentIndex);\n\n if (result) {\n alignedLines.push(result);\n }\n segmentIndex += segmentsConsumed;\n } else {\n // For lines that don't need alignment, use one-to-one correspondence\n alignedLines.push(segmentLines[segmentIndex]);\n segmentIndex++;\n }\n }\n\n // Add any remaining segments that were not processed\n if (segmentIndex < segmentLines.length) {\n alignedLines.push(...segmentLines.slice(segmentIndex));\n }\n\n return alignedLines;\n};\n\n/**\n * Tries to merge two candidate segments in both possible orders and returns the best match.\n *\n * @param targetLine - The line we are trying to reconstruct.\n * @param partA - The first candidate segment to evaluate.\n * @param partB - The second candidate segment to evaluate.\n * @returns The merged segment that best matches the target line after normalization.\n */\nconst findBestSegmentMerge = (targetLine: string, partA: string, partB: string) => {\n const mergedForward = `${partA} ${partB}`;\n const mergedReversed = `${partB} ${partA}`;\n\n const normalizedTarget = sanitizeArabic(targetLine);\n const scoreForward = calculateSimilarity(normalizedTarget, sanitizeArabic(mergedForward));\n const scoreReversed = calculateSimilarity(normalizedTarget, sanitizeArabic(mergedReversed));\n\n return scoreForward >= scoreReversed ? mergedForward : mergedReversed;\n};\n\n/**\n * Processes a single target line that needs alignment.\n *\n * @param targetLine - The line we are attempting to align to.\n * @param segmentLines - The collection of available text segments.\n * @param segmentIndex - The current index within {@link segmentLines} to consider.\n * @returns An object containing the resulting aligned text and how many segments were consumed.\n */\nconst processAlignmentTarget = (targetLine: string, segmentLines: string[], segmentIndex: number) => {\n const currentSegment = segmentLines[segmentIndex];\n\n // First, check if the current segment is already a good match\n if (areSimilarAfterNormalization(targetLine, currentSegment)) {\n return { result: currentSegment, segmentsConsumed: 1 };\n }\n\n // If not a direct match, try to merge two segments\n const partA = segmentLines[segmentIndex];\n const partB = segmentLines[segmentIndex + 1];\n\n // Ensure we have two parts to merge\n if (!partA || !partB) {\n return partA ? { result: partA, segmentsConsumed: 1 } : { result: '', segmentsConsumed: 0 };\n }\n\n const bestMerge = findBestSegmentMerge(targetLine, partA, partB);\n return { result: bestMerge, segmentsConsumed: 2 };\n};\n","/**\n * Represents an error found when checking balance of quotes or brackets in text.\n */\ntype BalanceError = {\n /** The character that caused the error */\n char: string;\n /** The position of the character in the string */\n index: number;\n /** The reason for the error */\n reason: 'mismatched' | 'unclosed' | 'unmatched';\n /** The type of character that caused the error */\n type: 'bracket' | 'quote';\n};\n\n/**\n * Result of a balance check operation.\n */\ntype BalanceResult = {\n /** Array of errors found during balance checking */\n errors: BalanceError[];\n /** Whether the text is properly balanced */\n isBalanced: boolean;\n};\n\n/**\n * Checks if all double quotes in a string are balanced and returns detailed error information.\n *\n * A string has balanced quotes when every opening quote has a corresponding closing quote.\n * This function counts all quote characters and determines if there's an even number of them.\n * If there's an odd number, the last quote is marked as unmatched.\n *\n * @param str - The string to check for quote balance\n * @returns An object containing balance status and any errors found\n *\n * @example\n * ```typescript\n * checkQuoteBalance('Hello \"world\"') // { errors: [], isBalanced: true }\n * checkQuoteBalance('Hello \"world') // { errors: [{ char: '\"', index: 6, reason: 'unmatched', type: 'quote' }], isBalanced: false }\n * ```\n */\nconst checkQuoteBalance = (str: string): BalanceResult => {\n const errors: BalanceError[] = [];\n let quoteCount = 0;\n let lastQuoteIndex = -1;\n\n for (let i = 0; i < str.length; i++) {\n if (str[i] === '\"') {\n quoteCount++;\n lastQuoteIndex = i;\n }\n }\n\n const isBalanced = quoteCount % 2 === 0;\n\n if (!isBalanced && lastQuoteIndex !== -1) {\n errors.push({\n char: '\"',\n index: lastQuoteIndex,\n reason: 'unmatched',\n type: 'quote',\n });\n }\n\n return { errors, isBalanced };\n};\n\n/** Mapping of opening brackets to their corresponding closing brackets */\nexport const BRACKETS = { 'ยซ': 'ยป', '(': ')', '[': ']', '{': '}' };\n\n/** Set of all opening bracket characters */\nexport const OPEN_BRACKETS = new Set(['ยซ', '(', '[', '{']);\n\n/** Set of all closing bracket characters */\nexport const CLOSE_BRACKETS = new Set(['ยป', ')', ']', '}']);\n\n/**\n * Checks if all brackets in a string are properly balanced and returns detailed error information.\n *\n * A string has balanced brackets when:\n * - Every opening bracket has a corresponding closing bracket\n * - Brackets are properly nested (no crossing pairs)\n * - Each closing bracket matches the most recent unmatched opening bracket\n *\n * Supports the following bracket pairs: (), [], {}, ยซยป\n *\n * @param str - The string to check for bracket balance\n * @returns An object containing balance status and any errors found\n *\n * @example\n * ```typescript\n * checkBracketBalance('(hello [world])') // { errors: [], isBalanced: true }\n * checkBracketBalance('(hello [world)') // { errors: [{ char: '[', index: 7, reason: 'unclosed', type: 'bracket' }], isBalanced: false }\n * checkBracketBalance('(hello ]world[') // { errors: [...], isBalanced: false }\n * ```\n */\nconst checkBracketBalance = (str: string): BalanceResult => {\n const errors: BalanceError[] = [];\n const stack: Array<{ char: string; index: number }> = [];\n\n for (let i = 0; i < str.length; i++) {\n const char = str[i];\n\n if (OPEN_BRACKETS.has(char)) {\n stack.push({ char, index: i });\n } else if (CLOSE_BRACKETS.has(char)) {\n const lastOpen = stack.pop();\n\n if (!lastOpen) {\n errors.push({\n char,\n index: i,\n reason: 'unmatched',\n type: 'bracket',\n });\n } else if (BRACKETS[lastOpen.char as keyof typeof BRACKETS] !== char) {\n errors.push({\n char: lastOpen.char,\n index: lastOpen.index,\n reason: 'mismatched',\n type: 'bracket',\n });\n errors.push({\n char,\n index: i,\n reason: 'mismatched',\n type: 'bracket',\n });\n }\n }\n }\n\n stack.forEach(({ char, index }) => {\n errors.push({\n char,\n index,\n reason: 'unclosed',\n type: 'bracket',\n });\n });\n\n return { errors, isBalanced: errors.length === 0 };\n};\n\n/**\n * Checks if both quotes and brackets are balanced in a string and returns detailed error information.\n *\n * This function combines the results of both quote and bracket balance checking,\n * providing a comprehensive analysis of all balance issues in the text.\n * The errors are sorted by their position in the string for easier debugging.\n *\n * @param str - The string to check for overall balance\n * @returns An object containing combined balance status and all errors found, sorted by position\n *\n * @example\n * ```typescript\n * checkBalance('Hello \"world\" and (test)') // { errors: [], isBalanced: true }\n * checkBalance('Hello \"world and (test') // { errors: [...], isBalanced: false }\n * ```\n */\nexport const checkBalance = (str: string): BalanceResult => {\n const quoteResult = checkQuoteBalance(str);\n const bracketResult = checkBracketBalance(str);\n\n return {\n errors: [...quoteResult.errors, ...bracketResult.errors].sort((a, b) => a.index - b.index),\n isBalanced: quoteResult.isBalanced && bracketResult.isBalanced,\n };\n};\n\n/**\n * Enhanced error detection that returns absolute character positions for use with HighlightableTextarea.\n *\n * This interface extends the basic BalanceError to include absolute positioning\n * across multiple lines of text, making it suitable for text editors and\n * syntax highlighters that need precise character positioning.\n */\nexport interface CharacterError {\n /** Absolute character position from the start of the entire text */\n absoluteIndex: number;\n /** The character that caused the error */\n char: string;\n /** The reason for the error */\n reason: 'mismatched' | 'unclosed' | 'unmatched';\n /** The type of character that caused the error */\n type: 'bracket' | 'quote';\n}\n\n/**\n * Gets detailed character-level errors for unbalanced quotes and brackets in multi-line text.\n *\n * This function processes text line by line, but only checks lines longer than 10 characters\n * for balance issues. It returns absolute positions that can be used with text editors\n * or highlighting components that need precise character positioning across the entire text.\n *\n * The absolute index accounts for newline characters between lines, providing accurate\n * positioning for the original text string.\n *\n * @param text - The multi-line text to analyze for balance errors\n * @returns Array of character errors with absolute positioning information\n *\n * @example\n * ```typescript\n * const text = 'Line 1 with \"quote\\nLine 2 with (bracket';\n * const errors = getUnbalancedErrors(text);\n * // Returns errors with absoluteIndex pointing to exact character positions\n * ```\n */\nexport const getUnbalancedErrors = (text: string): CharacterError[] => {\n const characterErrors: CharacterError[] = [];\n const lines = text.split('\\n');\n let absoluteIndex = 0;\n\n lines.forEach((line, lineIndex) => {\n if (line.length > 10) {\n const balanceResult = checkBalance(line);\n if (!balanceResult.isBalanced) {\n balanceResult.errors.forEach((error) => {\n characterErrors.push({\n absoluteIndex: absoluteIndex + error.index,\n char: error.char,\n reason: error.reason,\n type: error.type,\n });\n });\n }\n }\n // Add 1 for the newline character (except for the last line)\n absoluteIndex += line.length + (lineIndex < lines.length - 1 ? 1 : 0);\n });\n\n return characterErrors;\n};\n\n/**\n * Checks if all double quotes in a string are balanced.\n *\n * This is a convenience function that returns only the boolean result\n * without detailed error information.\n *\n * @param str - The string to check for quote balance\n * @returns True if quotes are balanced, false otherwise\n *\n * @example\n * ```typescript\n * areQuotesBalanced('Hello \"world\"') // true\n * areQuotesBalanced('Hello \"world') // false\n * ```\n */\nexport const areQuotesBalanced = (str: string): boolean => {\n return checkQuoteBalance(str).isBalanced;\n};\n\n/**\n * Checks if all brackets in a string are properly balanced.\n *\n * This is a convenience function that returns only the boolean result\n * without detailed error information.\n *\n * @param str - The string to check for bracket balance\n * @returns True if brackets are balanced, false otherwise\n *\n * @example\n * ```typescript\n * areBracketsBalanced('(hello [world])') // true\n * areBracketsBalanced('(hello [world') // false\n * ```\n */\nexport const areBracketsBalanced = (str: string): boolean => {\n return checkBracketBalance(str).isBalanced;\n};\n\n/**\n * Checks if both quotes and brackets are balanced in a string.\n *\n * This is a convenience function that returns only the boolean result\n * without detailed error information.\n *\n * @param str - The string to check for overall balance\n * @returns True if both quotes and brackets are balanced, false otherwise\n *\n * @example\n * ```typescript\n * isBalanced('Hello \"world\" and (test)') // true\n * isBalanced('Hello \"world and (test') // false\n * ```\n */\nexport const isBalanced = (str: string): boolean => {\n return checkBalance(str).isBalanced;\n};\n","export const INTAHA_ACTUAL = 'ุงูู';\n\n/**\n * Collection of regex patterns used throughout the library for text processing\n */\nexport const PATTERNS = {\n /** Matches Arabic characters across all Unicode blocks */\n arabicCharacters: /[\\u0600-\\u06FF\\u0750-\\u077F\\u08A0-\\u08FF\\uFB50-\\uFDFF\\uFE70-\\uFEFF]/,\n\n /** Matches Arabic-Indic digits (ู -ูฉ) and Western digits (0-9) */\n arabicDigits: /[0-9\\u0660-\\u0669]+/,\n\n /** Matches footnote references at the start of a line with Arabic-Indic digits: ^\\([\\u0660-\\u0669]+\\) */\n arabicFootnoteReferenceRegex: /^\\([\\u0660-\\u0669]+\\)/g,\n\n /** Matches Arabic letters and digits (both Western 0-9 and Arabic-Indic ู -ูฉ) */\n arabicLettersAndDigits: /[0-9\\u0621-\\u063A\\u0641-\\u064A\\u0660-\\u0669]+/g,\n\n /** Matches Arabic punctuation marks and whitespace characters */\n arabicPunctuationAndWhitespace: /[\\s\\u060C\\u061B\\u061F\\u06D4]+/,\n\n /** Matches footnote references with Arabic-Indic digits in parentheses: \\([\\u0660-\\u0669]+\\) */\n arabicReferenceRegex: /\\([\\u0660-\\u0669]+\\)/g,\n\n /** Matches embedded footnotes within text: \\([0-9\\u0660-\\u0669]+\\) */\n footnoteEmbedded: /\\([0-9\\u0660-\\u0669]+\\)/,\n\n /** Matches standalone footnote markers at line start/end: ^\\(?[0-9\\u0660-\\u0669]+\\)?[ุ.]?$ */\n footnoteStandalone: /^\\(?[0-9\\u0660-\\u0669]+\\)?[ุ.]?$/,\n\n /** Matches invalid/problematic footnote references: empty \"()\" or OCR-confused endings */\n invalidReferenceRegex: /\\(\\)|\\([.1OV9]+\\)/g, // Combined pattern for detecting any invalid/problematic references\n\n /** Matches OCR-confused footnote references at line start with characters like .1OV9 */\n ocrConfusedFootnoteReferenceRegex: /^\\([.1OV9]+\\)/g,\n\n /** Matches OCR-confused footnote references with characters commonly misread as Arabic digits */\n ocrConfusedReferenceRegex: /\\([.1OV9]+\\)/g,\n\n /** Matches one or more whitespace characters */\n whitespace: /\\s+/,\n};\n\n/**\n * Extracts the first sequence of Arabic or Western digits from text.\n * Used primarily for footnote number comparison to match related footnote elements.\n *\n * @param text - Text containing digits to extract\n * @returns First digit sequence found, or empty string if none found\n * @example\n * extractDigits('(ูฅ)ุฃุฎุฑุฌู ุงูุจุฎุงุฑู') // Returns 'ูฅ'\n * extractDigits('See note (123)') // Returns '123'\n */\nexport const extractDigits = (text: string): string => {\n const match = text.match(PATTERNS.arabicDigits);\n return match ? match[0] : '';\n};\n\n/**\n * Tokenizes text into individual words while preserving special symbols.\n * Adds spacing around preserved symbols to ensure they are tokenized separately,\n * then splits on whitespace.\n *\n * @param text - Text to tokenize\n * @param preserveSymbols - Array of symbols that should be tokenized as separate tokens\n * @returns Array of tokens, or empty array if input is empty/whitespace\n * @example\n * tokenizeText('Hello ๏ทบ world', ['๏ทบ']) // Returns ['Hello', '๏ทบ', 'world']\n */\nexport const tokenizeText = (text: string, preserveSymbols: string[] = []): string[] => {\n let processedText = text;\n\n // Add spaces around each preserve symbol to ensure they're tokenized separately\n for (const symbol of preserveSymbols) {\n const symbolRegex = new RegExp(symbol, 'g');\n processedText = processedText.replace(symbolRegex, ` ${symbol} `);\n }\n\n return processedText.trim().split(PATTERNS.whitespace).filter(Boolean);\n};\n\n/**\n * Handles fusion of standalone and embedded footnotes during token processing.\n * Detects patterns where standalone footnotes should be merged with embedded ones\n * or where trailing standalone footnotes should be skipped.\n *\n * @param result - Current result array being built\n * @param previousToken - The previous token in the sequence\n * @param currentToken - The current token being processed\n * @returns True if the current token was handled (fused or skipped), false otherwise\n * @example\n * // (ูฅ) + (ูฅ)ุฃุฎุฑุฌู โ result gets (ูฅ)ุฃุฎุฑุฌู\n * // (ูฅ)ุฃุฎุฑุฌู + (ูฅ) โ (ูฅ) is skipped\n */\nexport const handleFootnoteFusion = (result: string[], previousToken: string, currentToken: string): boolean => {\n const prevIsStandalone = PATTERNS.footnoteStandalone.test(previousToken);\n const currHasEmbedded = PATTERNS.footnoteEmbedded.test(currentToken);\n const currIsStandalone = PATTERNS.footnoteStandalone.test(currentToken);\n const prevHasEmbedded = PATTERNS.footnoteEmbedded.test(previousToken);\n\n const prevDigits = extractDigits(previousToken);\n const currDigits = extractDigits(currentToken);\n\n // Replace standalone with fused version: (ูฅ) + (ูฅ)ุฃุฎุฑุฌู โ (ูฅ)ุฃุฎุฑุฌู\n if (prevIsStandalone && currHasEmbedded && prevDigits === currDigits) {\n result[result.length - 1] = currentToken;\n return true;\n }\n\n // Skip trailing standalone: (ูฅ)ุฃุฎุฑุฌู + (ูฅ) โ (ูฅ)ุฃุฎุฑุฌู\n if (prevHasEmbedded && currIsStandalone && prevDigits === currDigits) {\n return true;\n }\n\n return false;\n};\n\n/**\n * Handles selection logic for tokens with embedded footnotes during alignment.\n * Prefers tokens that contain embedded footnotes over plain text, and among\n * tokens with embedded footnotes, prefers the shorter one.\n *\n * @param tokenA - First token to compare\n * @param tokenB - Second token to compare\n * @returns Array containing selected token(s), or null if no special handling needed\n * @example\n * handleFootnoteSelection('text', '(ูก)text') // Returns ['(ูก)text']\n * handleFootnoteSelection('(ูก)longtext', '(ูก)text') // Returns ['(ูก)text']\n */\nexport const handleFootnoteSelection = (tokenA: string, tokenB: string): null | string[] => {\n const aHasEmbedded = PATTERNS.footnoteEmbedded.test(tokenA);\n const bHasEmbedded = PATTERNS.footnoteEmbedded.test(tokenB);\n\n if (aHasEmbedded && !bHasEmbedded) {\n return [tokenA];\n }\n if (bHasEmbedded && !aHasEmbedded) {\n return [tokenB];\n }\n if (aHasEmbedded && bHasEmbedded) {\n return [tokenA.length <= tokenB.length ? tokenA : tokenB];\n }\n\n return null;\n};\n\n/**\n * Handles selection logic for standalone footnote tokens during alignment.\n * Manages cases where one or both tokens are standalone footnotes, preserving\n * both tokens when one is a footnote and the other is regular text.\n *\n * @param tokenA - First token to compare\n * @param tokenB - Second token to compare\n * @returns Array containing selected token(s), or null if no special handling needed\n * @example\n * handleStandaloneFootnotes('(ูก)', 'text') // Returns ['(ูก)', 'text']\n * handleStandaloneFootnotes('(ูก)', '(ูข)') // Returns ['(ูก)'] (shorter one)\n */\nexport const handleStandaloneFootnotes = (tokenA: string, tokenB: string): null | string[] => {\n const aIsFootnote = PATTERNS.footnoteStandalone.test(tokenA);\n const bIsFootnote = PATTERNS.footnoteStandalone.test(tokenB);\n\n if (aIsFootnote && !bIsFootnote) {\n return [tokenA, tokenB];\n }\n if (bIsFootnote && !aIsFootnote) {\n return [tokenB, tokenA];\n }\n if (aIsFootnote && bIsFootnote) {\n return [tokenA.length <= tokenB.length ? tokenA : tokenB];\n }\n\n return null;\n};\n\n/**\n * Removes simple footnote references from Arabic text.\n * Handles footnotes in the format (ยฌ[Arabic numerals]) where ยฌ is the not symbol (U+00AC).\n *\n * @param text - The input text containing footnote references to remove\n * @returns The text with footnote references removed and extra spaces normalized\n *\n * @example\n * ```typescript\n * removeFootnoteReferencesSimple(\"ูุฐุง ุงููุต (ยฌูกูขูฃ) ูุญุชูู ุนูู ุญุงุดูุฉ\")\n * // Returns: \"ูุฐุง ุงููุต ูุญุชูู ุนูู ุญุงุดูุฉ\"\n * ```\n */\nexport const removeFootnoteReferencesSimple = (text: string): string => {\n return text\n .replace(/ ?\\(\\u00AC[\\u0660-\\u0669]+\\) ?/g, ' ')\n .replace(/ +/g, ' ')\n .trim();\n};\n\n/**\n * Removes single digit footnote references and extended footnote formats from Arabic text.\n * Handles footnotes in the format:\n * - ([single Arabic digit]) - e.g., (ูฃ)\n * - ([single Arabic digit] [single Arabic letter]) - e.g., (ูฃ ู
), (ูฅ ู), (ูง ุจ)\n *\n * @param text - The input text containing footnote references to remove\n * @returns The text with footnote references removed and extra spaces normalized\n *\n * @example\n * ```typescript\n * removeSingleDigitFootnoteReferences(\"ูุฐุง ุงููุต (ูฃ) ูุงูุขุฎุฑ (ูฅ ู
) ูุงูุซุงูุซ (ูง ู) ูุญุชูู ุนูู ุญูุงุดู\")\n * // Returns: \"ูุฐุง ุงููุต ูุงูุขุฎุฑ ูุงูุซุงูุซ ูุญุชูู ุนูู ุญูุงุดู\"\n * ```\n */\nexport const removeSingleDigitFootnoteReferences = (text: string): string => {\n // Remove single digit footnotes with optional Arabic letter suffix: (ูฃ) or (ูฃ ู
) or (ูฅ ู) etc.\n return text\n .replace(/ ?\\([ู -ูฉ]{1}(\\s+[\\u0600-\\u06FF])?\\) ?/g, ' ')\n .replace(/ +/g, ' ')\n .trim();\n};\n\n/**\n * Standardizes standalone Hijri symbol ู to ูู when following Arabic digits\n * @param text - Input text to process\n * @returns Text with standardized Hijri symbols\n */\nexport const standardizeHijriSymbol = (text: string) => {\n // Replace standalone ู with ูู when it appears after Arabic digits (0-9 or ู -ูฉ)\n // Allow any amount of whitespace between the digit and ู, and consider Arabic punctuation as a boundary.\n // Boundary rule: only Arabic letters/digits should block replacement; punctuation should not.\n return text.replace(/([0-9\\u0660-\\u0669])\\s*ู(?=\\s|$|[^\\u0621-\\u063A\\u0641-\\u064A\\u0660-\\u0669])/gu, '$1 ูู');\n};\n\n/**\n * Standardizes standalone ุงู to ุงูู when appearing as whole word\n * @param text - Input text to process\n * @returns Text with standardized AH Hijri symbols\n */\nexport const standardizeIntahaSymbol = (text: string) => {\n // Replace standalone ุงู with ุงูู when it appears as a whole word\n // Ensures it's preceded by start/whitespace/non-Arabic AND followed by end/whitespace/non-Arabic\n return text.replace(/(^|\\s|[^\\u0600-\\u06FF])ุงู(?=\\s|$|[^\\u0600-\\u06FF])/gu, `$1${INTAHA_ACTUAL}`);\n};\n","import { PATTERNS } from './utils/textUtils';\n\nconst INVALID_FOOTNOTE = '()';\n\n/**\n * Checks if the given text contains invalid footnote references.\n * Invalid footnotes include empty parentheses \"()\" or OCR-confused characters\n * like \".1OV9\" that were misrecognized instead of Arabic numerals.\n *\n * @param text - Text to check for invalid footnote patterns\n * @returns True if text contains invalid footnote references, false otherwise\n * @example\n * hasInvalidFootnotes('This text has ()') // Returns true\n * hasInvalidFootnotes('This text has (ูก)') // Returns false\n * hasInvalidFootnotes('OCR mistake (O)') // Returns true\n */\nexport const hasInvalidFootnotes = (text: string): boolean => {\n return PATTERNS.invalidReferenceRegex.test(text);\n};\n\n// Arabic number formatter instance\nconst arabicFormatter = new Intl.NumberFormat('ar-SA');\n\n/**\n * Converts a number to Arabic-Indic numerals using the Intl.NumberFormat API.\n * Uses the 'ar-SA' locale to ensure proper Arabic numeral formatting.\n *\n * @param num - The number to convert to Arabic numerals\n * @returns String representation using Arabic-Indic digits (ู -ูฉ)\n * @example\n * numberToArabic(123) // Returns 'ูกูขูฃ'\n * numberToArabic(5) // Returns 'ูฅ'\n */\nconst numberToArabic = (num: number): string => {\n return arabicFormatter.format(num);\n};\n\n/**\n * Converts OCR-confused characters to their corresponding Arabic-Indic numerals.\n * Handles common OCR misrecognitions where Latin characters are mistaken for Arabic digits.\n *\n * @param char - Single character that may be an OCR mistake\n * @returns Corresponding Arabic-Indic numeral or original character if no mapping exists\n * @example\n * ocrToArabic('O') // Returns 'ูฅ' (O often confused with ูฅ)\n * ocrToArabic('1') // Returns 'ูก' (1 often confused with ูก)\n * ocrToArabic('.') // Returns 'ู ' (dot often confused with ู )\n */\nconst ocrToArabic = (char: string): string => {\n const ocrToArabicMap: { [key: string]: string } = {\n '1': 'ูก',\n '9': 'ูฉ',\n '.': 'ู ',\n O: 'ูฅ',\n o: 'ูฅ',\n V: 'ูง',\n v: 'ูง',\n };\n return ocrToArabicMap[char] || char;\n};\n\n/**\n * Parses Arabic-Indic numerals from a reference string and converts to a JavaScript number.\n * Removes parentheses and converts each Arabic-Indic digit to its Western equivalent.\n *\n * @param arabicStr - String containing Arabic-Indic numerals, typically in format '(ูกูขูฃ)'\n * @returns Parsed number, or 0 if parsing fails\n * @example\n * arabicToNumber('(ูกูขูฃ)') // Returns 123\n * arabicToNumber('(ูฅ)') // Returns 5\n * arabicToNumber('invalid') // Returns 0\n */\nconst arabicToNumber = (arabicStr: string): number => {\n const lookup: { [key: string]: string } = {\n 'ู ': '0',\n 'ูก': '1',\n 'ูข': '2',\n 'ูฃ': '3',\n 'ูค': '4',\n 'ูฅ': '5',\n 'ูฆ': '6',\n 'ูง': '7',\n 'ูจ': '8',\n 'ูฉ': '9',\n };\n const digits = arabicStr.replace(/[()]/g, '');\n let numStr = '';\n for (const char of digits) {\n numStr += lookup[char];\n }\n const parsed = parseInt(numStr, 10);\n return Number.isNaN(parsed) ? 0 : parsed;\n};\n\ntype TextLine = {\n isFootnote?: boolean;\n text: string;\n};\n\n/**\n * Extracts all footnote references from text lines, categorizing them by type and location.\n * Handles both Arabic-Indic numerals and OCR-confused characters in body text and footnotes.\n *\n * @param lines - Array of text line objects with optional isFootnote flag\n * @returns Object containing categorized reference arrays:\n * - bodyReferences: All valid references found in body text\n * - footnoteReferences: All valid references found in footnotes\n * - ocrConfusedInBody: OCR-confused references in body text (for tracking)\n * - ocrConfusedInFootnotes: OCR-confused references in footnotes (for tracking)\n * @example\n * const lines = [\n * { text: 'Body with (ูก) and (O)', isFootnote: false },\n * { text: '(ูก) Footnote text', isFootnote: true }\n * ];\n * const refs = extractReferences(lines);\n * // refs.bodyReferences contains ['(ูก)', '(ูฅ)'] - OCR 'O' converted to 'ูฅ'\n */\nconst extractReferences = (lines: TextLine[]) => {\n const arabicReferencesInBody = lines\n .filter((b) => !b.isFootnote)\n .flatMap((b) => b.text.match(PATTERNS.arabicReferenceRegex) || []);\n\n const ocrConfusedReferencesInBody = lines\n .filter((b) => !b.isFootnote)\n .flatMap((b) => b.text.match(PATTERNS.ocrConfusedReferenceRegex) || []);\n\n const arabicReferencesInFootnotes = lines\n .filter((b) => b.isFootnote)\n .flatMap((b) => b.text.match(PATTERNS.arabicFootnoteReferenceRegex) || []);\n\n const ocrConfusedReferencesInFootnotes = lines\n .filter((b) => b.isFootnote)\n .flatMap((b) => b.text.match(PATTERNS.ocrConfusedFootnoteReferenceRegex) || []);\n\n const convertedOcrBodyRefs = ocrConfusedReferencesInBody.map((ref) =>\n ref.replace(/[.1OV9]/g, (char) => ocrToArabic(char)),\n );\n\n const convertedOcrFootnoteRefs = ocrConfusedReferencesInFootnotes.map((ref) =>\n ref.replace(/[.1OV9]/g, (char) => ocrToArabic(char)),\n );\n\n return {\n bodyReferences: [...arabicReferencesInBody, ...convertedOcrBodyRefs],\n footnoteReferences: [...arabicReferencesInFootnotes, ...convertedOcrFootnoteRefs],\n ocrConfusedInBody: ocrConfusedReferencesInBody,\n ocrConfusedInFootnotes: ocrConfusedReferencesInFootnotes,\n };\n};\n\n/**\n * Determines if footnote reference correction is needed by checking for:\n * 1. Invalid footnote patterns (empty parentheses, OCR mistakes)\n * 2. Mismatched sets of references between body text and footnotes\n * 3. Different counts of references in body vs footnotes\n *\n * @param lines - Array of text line objects to analyze\n * @param references - Extracted reference data from extractReferences()\n * @returns True if correction is needed, false if references are already correct\n * @example\n * const lines = [{ text: 'Text with ()', isFootnote: false }];\n * const refs = extractReferences(lines);\n * needsCorrection(lines, refs) // Returns true due to invalid \"()\" reference\n */\nconst needsCorrection = (lines: TextLine[], references: ReturnType<typeof extractReferences>) => {\n const mistakenReferences = lines.some((line) => hasInvalidFootnotes(line.text));\n if (mistakenReferences) {\n return true;\n }\n\n const bodySet = new Set(references.bodyReferences);\n const footnoteSet = new Set(references.footnoteReferences);\n if (bodySet.size !== footnoteSet.size) {\n return true;\n }\n\n // Check if the sets contain the same elements\n for (const ref of bodySet) {\n if (!footnoteSet.has(ref)) {\n return true;\n }\n }\n\n return false;\n};\n\n/**\n * Corrects footnote references in an array of text lines by:\n * 1. Converting OCR-confused characters to proper Arabic numerals\n * 2. Filling in empty \"()\" references with appropriate numbers\n * 3. Ensuring footnote references in body text match those in footnotes\n * 4. Generating new reference numbers when needed\n *\n * @param lines - Array of text line objects, each with optional isFootnote flag\n * @returns Array of corrected text lines with proper footnote references\n * @example\n * const lines = [\n * { text: 'Main text with ()', isFootnote: false },\n * { text: '() This is a footnote', isFootnote: true }\n * ];\n * const corrected = correctReferences(lines);\n * // Returns lines with \"()\" replaced by proper Arabic numerals like \"(ูก)\"\n */\nexport const correctReferences = <T extends TextLine>(lines: T[]): T[] => {\n const initialReferences = extractReferences(lines);\n\n if (!needsCorrection(lines, initialReferences)) {\n return lines;\n }\n\n // Pass 1: Sanitize lines by correcting only OCR characters inside reference markers.\n const sanitizedLines = lines.map((line) => {\n let updatedText = line.text;\n // This regex finds the full reference, e.g., \"(O)\" or \"(1)\"\n const ocrRegex = /\\([.1OV9]+\\)/g;\n updatedText = updatedText.replace(ocrRegex, (match) => {\n // This replace acts *inside* the found match, e.g., on \"O\" or \"1\"\n return match.replace(/[.1OV9]/g, (char) => ocrToArabic(char));\n });\n return { ...line, text: updatedText };\n });\n\n // Pass 2: Analyze the sanitized lines to get a clear and accurate picture of references.\n const cleanReferences = extractReferences(sanitizedLines);\n\n // Step 3: Create queues of \"unmatched\" references for two-way pairing.\n const bodyRefSet = new Set(cleanReferences.bodyReferences);\n const footnoteRefSet = new Set(cleanReferences.footnoteReferences);\n\n const uniqueBodyRefs = [...new Set(cleanReferences.bodyReferences)];\n const uniqueFootnoteRefs = [...new Set(cleanReferences.footnoteReferences)];\n\n // Queue 1: Body references available for footnotes.\n const bodyRefsForFootnotes = uniqueBodyRefs.filter((ref) => !footnoteRefSet.has(ref));\n // Queue 2: Footnote references available for the body.\n const footnoteRefsForBody = uniqueFootnoteRefs.filter((ref) => !bodyRefSet.has(ref));\n\n // Step 4: Determine the starting point for any completely new reference numbers.\n const allRefs = [...bodyRefSet, ...footnoteRefSet];\n const maxRefNum = allRefs.length > 0 ? Math.max(0, ...allRefs.map((ref) => arabicToNumber(ref))) : 0;\n const referenceCounter = { count: maxRefNum + 1 };\n\n // Step 5: Map over the sanitized lines, filling in '()' using the queues.\n return sanitizedLines.map((line) => {\n if (!line.text.includes(INVALID_FOOTNOTE)) {\n return line;\n }\n let updatedText = line.text;\n\n updatedText = updatedText.replace(/\\(\\)/g, () => {\n if (line.isFootnote) {\n const availableRef = bodyRefsForFootnotes.shift();\n if (availableRef) {\n return availableRef;\n }\n } else {\n // It's body text\n const availableRef = footnoteRefsForBody.shift();\n if (availableRef) {\n return availableRef;\n }\n }\n\n // If no available partner reference exists, generate a new one.\n const newRef = `(${numberToArabic(referenceCounter.count)})`;\n referenceCounter.count++;\n return newRef;\n });\n\n return { ...line, text: updatedText };\n });\n};\n","/**\n * Node in the Aho-Corasick automaton trie structure.\n * Each node represents a state in the pattern matching automaton.\n */\nclass ACNode {\n /** Transition map from characters to next node indices */\n next: Map<string, number> = new Map();\n /** Failure link for efficient pattern matching */\n link = 0;\n /** Pattern IDs that end at this node */\n out: number[] = [];\n}\n\n/**\n * Aho-Corasick automaton for efficient multi-pattern string matching.\n * Provides O(n + m + z) time complexity where n is text length,\n * m is total pattern length, and z is number of matches.\n */\nexport class AhoCorasick {\n /** Array of nodes forming the automaton */\n private nodes: ACNode[] = [new ACNode()];\n\n /**\n * Adds a pattern to the automaton trie.\n *\n * @param pattern - Pattern string to add\n * @param id - Unique identifier for this pattern\n */\n add(pattern: string, id: number): void {\n let v = 0;\n for (let i = 0; i < pattern.length; i++) {\n const ch = pattern[i];\n let to = this.nodes[v].next.get(ch);\n if (to === undefined) {\n to = this.nodes.length;\n this.nodes[v].next.set(ch, to);\n this.nodes.push(new ACNode());\n }\n v = to;\n }\n this.nodes[v].out.push(id);\n }\n\n /**\n * Builds failure links for the automaton using BFS.\n * Must be called after adding all patterns and before searching.\n */\n build(): void {\n const q: number[] = [];\n for (const [, to] of this.nodes[0].next) {\n this.nodes[to].link = 0;\n q.push(to);\n }\n for (let qi = 0; qi < q.length; qi++) {\n const v = q[qi]!;\n\n for (const [ch, to] of this.nodes[v].next) {\n q.push(to);\n let link = this.nodes[v].link;\n while (link !== 0 && !this.nodes[link].next.has(ch)) {\n link = this.nodes[link].link;\n }\n const nxt = this.nodes[link].next.get(ch);\n this.nodes[to].link = nxt === undefined ? 0 : nxt;\n const linkOut = this.nodes[this.nodes[to].link].out;\n if (linkOut.length) {\n this.nodes[to].out.push(...linkOut);\n }\n }\n }\n }\n\n /**\n * Finds all pattern matches in the given text.\n *\n * @param text - Text to search in\n * @param onMatch - Callback function called for each match found\n * Receives pattern ID and end position of the match\n */\n find(text: string, onMatch: (patternId: number, endPos: number) => void): void {\n let v = 0;\n for (let i = 0; i < text.length; i++) {\n const ch = text[i];\n while (v !== 0 && !this.nodes[v].next.has(ch)) {\n v = this.nodes[v].link;\n }\n const to = this.nodes[v].next.get(ch);\n v = to === undefined ? 0 : to;\n if (this.nodes[v].out.length) {\n for (const pid of this.nodes[v].out) {\n onMatch(pid, i + 1);\n }\n }\n }\n }\n}\n\n/**\n * Builds Aho-Corasick automaton for exact pattern matching.\n *\n * @param patterns - Array of patterns to search for\n * @returns Constructed and built Aho-Corasick automaton ready for searching\n *\n * @example\n * ```typescript\n * const patterns = ['hello', 'world', 'hell'];\n * const ac = buildAhoCorasick(patterns);\n * ac.find('hello world', (patternId, endPos) => {\n * console.log(`Found pattern ${patternId} ending at position ${endPos}`);\n * });\n * ```\n */\nexport const buildAhoCorasick = (patterns: string[]) => {\n const ac = new AhoCorasick();\n for (let pid = 0; pid < patterns.length; pid++) {\n const pat = patterns[pid];\n if (pat.length > 0) {\n ac.add(pat, pid);\n }\n }\n ac.build();\n return ac;\n};\n","import type { MatchPolicy } from '@/types';\n\nexport const DEFAULT_POLICY: Required<MatchPolicy> = {\n enableFuzzy: true,\n gramsPerExcerpt: 5,\n log: () => {},\n maxCandidatesPerExcerpt: 40,\n maxEditAbs: 3,\n maxEditRel: 0.1,\n q: 4,\n seamLen: 512,\n};\n","import { buildAhoCorasick } from './ahocorasick';\nimport { boundedLevenshtein } from './levenshthein';\n\nconst SEAM_GAP_CEILING = 200; // max chars we are willing to skip at a boundary\nconst SEAM_BONUS_CAP = 80; // extra edit distance allowed for cross-page cases\n\n/**\n * Builds a concatenated book from pages with position tracking\n */\nexport function buildBook(pagesN: string[]) {\n const parts: string[] = [];\n const starts: number[] = [];\n const lens: number[] = [];\n let off = 0;\n\n for (let i = 0; i < pagesN.length; i++) {\n const p = pagesN[i];\n starts.push(off);\n lens.push(p.length);\n parts.push(p);\n off += p.length;\n\n if (i + 1 < pagesN.length) {\n parts.push(' '); // single space to allow cross-page substring matches\n off += 1;\n }\n }\n return { book: parts.join(''), lens, starts };\n}\n\n/**\n * Binary search to find which page contains a given position\n */\nexport function posToPage(pos: number, pageStarts: number[]): number {\n let lo = 0;\n let hi = pageStarts.length - 1;\n let ans = 0;\n\n while (lo <= hi) {\n const mid = (lo + hi) >> 1;\n if (pageStarts[mid] <= pos) {\n ans = mid;\n lo = mid + 1;\n } else {\n hi = mid - 1;\n }\n }\n return ans;\n}\n\n/**\n * Performs exact matching using Aho-Corasick algorithm to find all occurrences\n * of patterns in the concatenated book text.\n *\n * @param book - Concatenated text from all pages\n * @param pageStarts - Array of starting positions for each page in the book\n * @param patterns - Array of deduplicated patterns to search for\n * @param patIdToOrigIdxs - Mapping from pattern IDs to original excerpt indices\n * @param excerpts - Original array of excerpts (used for length reference)\n * @returns Object containing result array and exact match flags\n */\nexport function findExactMatches(\n book: string,\n pageStarts: number[],\n patterns: string[],\n patIdToOrigIdxs: number[][],\n excerptsCount: number,\n): { result: Int32Array; seenExact: Uint8Array } {\n const ac = buildAhoCorasick(patterns);\n const result = new Int32Array(excerptsCount).fill(-1);\n const seenExact = new Uint8Array(excerptsCount);\n\n ac.find(book, (pid, endPos) => {\n const pat = patterns[pid];\n const startPos = endPos - pat.length;\n const startPage = posToPage(startPos, pageStarts);\n\n for (const origIdx of patIdToOrigIdxs[pid]) {\n if (!seenExact[origIdx]) {\n result[origIdx] = startPage;\n seenExact[origIdx] = 1;\n }\n }\n });\n\n return { result, seenExact };\n}\n\n/**\n * Deduplicates excerpts and creates pattern mapping\n */\nexport function deduplicateExcerpts(excerptsN: string[]) {\n const keyToPatId = new Map<string, number>();\n const patIdToOrigIdxs: number[][] = [];\n const patterns: string[] = [];\n\n for (let i = 0; i < excerptsN.length; i++) {\n const k = excerptsN[i];\n let pid = keyToPatId.get(k);\n\n if (pid === undefined) {\n pid = patterns.length;\n keyToPatId.set(k, pid);\n patterns.push(k);\n patIdToOrigIdxs.push([i]);\n } else {\n patIdToOrigIdxs[pid].push(i);\n }\n }\n\n return { keyToPatId, patIdToOrigIdxs, patterns };\n}\n\n/**\n * Calculates fuzzy match score for a candidate using bounded Levenshtein distance.\n * Extracts a window around the candidate position and computes edit distance.\n *\n * @param excerpt - Text excerpt to match\n * @param candidate - Candidate position to evaluate\n * @param pagesN - Array of normalized page texts\n * @param seams - Array of seam data\n * @param maxDist - Maximum edit distance to consider\n * @returns Edit distance if within bounds, null otherwise\n */\nexport const calculateFuzzyScore = (\n excerpt: string,\n candidate: { page: number; seam: boolean; start: number },\n pagesN: string[],\n seams: { text: string }[],\n maxDist: number,\n) => {\n const L = excerpt.length;\n const extra = Math.min(maxDist, Math.max(6, Math.ceil(L * 0.12)));\n const half = Math.floor(extra / 2);\n const start0 = candidate.start - half;\n\n const base = candidate.seam ? seams[candidate.page]?.text : pagesN[candidate.page];\n if (!base) {\n return null;\n }\n\n const buildWindow = createWindowBuilder(candidate, pagesN, seams, start0, L, extra);\n const windows = generateWindows(buildWindow, candidate, base, start0, L, extra);\n\n const acceptance = calculateAcceptance(candidate, base, start0, L, extra, maxDist);\n return findBestMatch(windows, excerpt, acceptance);\n};\n\n/**\n * Creates a window builder function for the given candidate\n */\nconst createWindowBuilder = (\n candidate: { page: number; seam: boolean; start: number },\n pagesN: string[],\n seams: { text: string }[],\n start0: number,\n L: number,\n extra: number,\n) => {\n return (trimTailEndBy: number = 0, trimHeadStartBy: number = 0): string | null => {\n if (candidate.seam) {\n return buildSeamWindow(seams, candidate.page, start0, L, extra);\n }\n return buildPageWindow(pagesN, candidate.page, start0, L, extra, trimTailEndBy, trimHeadStartBy);\n };\n};\n\n/**\n * Builds a window from seam text\n */\nconst buildSeamWindow = (\n seams: { text: string }[],\n page: number,\n start0: number,\n L: number,\n extra: number,\n): string | null => {\n const seam = seams[page]?.text;\n if (!seam) {\n return null;\n }\n\n const s0 = Math.max(0, start0);\n const desired = L + extra;\n const end = Math.min(seam.length, s0 + desired);\n return end > s0 ? seam.slice(s0, end) : null;\n};\n\n/**\n * Builds a window from page text, potentially spanning multiple pages\n */\nconst buildPageWindow = (\n pagesN: string[],\n page: number,\n start0: number,\n L: number,\n extra: number,\n trimTailEndBy: number,\n trimHeadStartBy: number,\n): string | null => {\n const base = pagesN[page];\n if (!base) {\n return null;\n }\n\n const desired = L + extra;\n let s0 = start0;\n let window = '';\n\n // Prepend from previous pages if needed\n if (s0 < 0) {\n const needFromPrev = Math.max(0, -s0 - trimHeadStartBy);\n if (needFromPrev > 0) {\n window += buildPreviousPagesContent(pagesN, page, needFromPrev);\n }\n s0 = 0;\n }\n\n // Take from current page\n const end0 = Math.min(base.length - trimTailEndBy, Math.max(0, s0) + desired - window.length);\n if (end0 > s0) {\n window += base.slice(Math.max(0, s0), end0);\n }\n\n // Append from following pages\n window += buildFollowingPagesContent(pagesN, page, desired - window.length);\n\n return window.length ? window : null;\n};\n\n/**\n * Builds content from previous pages\n */\nconst buildPreviousPagesContent = (pagesN: string[], currentPage: number, needed: number): string => {\n let needPre = needed;\n let pp = currentPage - 1;\n const bits: string[] = [];\n\n while (needPre > 0 && pp >= 0) {\n const src = pagesN[pp];\n if (!src) {\n break;\n }\n\n const take = Math.min(needPre, src.length);\n const chunk = src.slice(src.length - take);\n bits.unshift(chunk);\n needPre -= chunk.length;\n pp--;\n }\n\n return bits.length ? `${bits.join(' ')} ` : '';\n};\n\n/**\n * Builds content from following pages\n */\nconst buildFollowingPagesContent = (pagesN: string[], currentPage: number, remaining: number): string => {\n let content = '';\n let pn = currentPage + 1;\n\n while (remaining > 0 && pn < pagesN.length) {\n const src = pagesN[pn];\n if (!src) {\n break;\n }\n\n const addition = src.slice(0, remaining);\n if (!addition.length) {\n break;\n }\n\n content += ` ${addition}`;\n remaining -= addition.length;\n pn++;\n }\n\n return content;\n};\n\n/**\n * Generates all possible windows for matching\n */\nconst generateWindows = (\n buildWindow: (trimTail: number, trimHead: number) => string | null,\n candidate: { page: number; seam: boolean; start: number },\n base: string,\n start0: number,\n L: number,\n extra: number,\n): string[] => {\n const windows: string[] = [];\n const desired = L + extra;\n const crossesEnd = !candidate.seam && start0 + desired > base.length;\n const crossesStart = !candidate.seam && start0 < 0;\n\n // Primary window\n const w0 = buildWindow(0, 0);\n if (w0) {\n windows.push(w0);\n }\n\n // Trimmed tail window if crossing end\n if (crossesEnd) {\n const cut = Math.min(SEAM_GAP_CEILING, Math.max(0, base.length - Math.max(0, start0)));\n if (cut > 0) {\n const wTrimTail = buildWindow(cut, 0);\n if (wTrimTail) {\n windows.push(wTrimTail);\n }\n }\n }\n\n // Trimmed head window if crossing start\n if (crossesStart) {\n const wTrimHead = buildWindow(0, Math.min(SEAM_GAP_CEILING, -start0));\n if (wTrimHead) {\n windows.push(wTrimHead);\n }\n }\n\n return windows;\n};\n\n/**\n * Calculates the acceptance threshold for edit distance\n */\nconst calculateAcceptance = (\n candidate: { page: number; seam: boolean; start: number },\n base: string,\n start0: number,\n L: number,\n extra: number,\n maxDist: number,\n): number => {\n const desired = L + extra;\n const crossesEnd = !candidate.seam && start0 + desired > base.length;\n const crossesStart = !candidate.seam && start0 < 0;\n\n const normalizationSlack = Math.min(2, Math.max(1, Math.ceil(L * 0.005)));\n\n return crossesEnd || crossesStart || candidate.seam\n ? maxDist + Math.min(SEAM_BONUS_CAP, Math.ceil(L * 0.08))\n : maxDist + normalizationSlack;\n};\n\n/**\n * Finds the best match among all windows\n */\nconst findBestMatch = (\n windows: string[],\n excerpt: string,\n acceptance: number,\n): { acceptance: number; dist: number } | null => {\n let best: number | null = null;\n\n for (const w of windows) {\n const d = boundedLevenshtein(excerpt, w, acceptance);\n if (d <= acceptance && (best == null || d < best)) {\n best = d;\n }\n }\n\n return best == null ? null : { acceptance, dist: best };\n};\n","/**\n * Represents a posting in the inverted index, storing position information.\n */\ntype Posting = {\n /** Page number where this gram occurs */\n page: number;\n /** Position within the page where this gram starts */\n pos: number;\n /** Whether this posting is from a seam (cross-page boundary) */\n seam: boolean;\n};\n\n/**\n * Basic gram information with position offset.\n */\ntype GramBase = {\n /** The q-gram string */\n gram: string;\n /** Offset position of this gram in the original text */\n offset: number;\n};\n\n/**\n * Extended gram information including frequency data for selection.\n */\ntype GramItem = GramBase & {\n /** Frequency count of this gram in the corpus */\n freq: number;\n};\n\n/**\n * Q-gram index for efficient fuzzy string matching candidate generation.\n * Maintains an inverted index of q-grams to their occurrence positions.\n */\nexport class QGramIndex {\n /** Length of q-grams to index */\n\n private q: number;\n /** Inverted index mapping q-grams to their postings */\n\n private map = new Map<string, Posting[]>();\n /** Frequency count for each q-gram in the corpus */\n\n private gramFreq = new Map<string, number>();\n\n /**\n * Creates a new Q-gram index with the specified gram length.\n * @param q - Length of q-grams to index (typically 3-5)\n */\n constructor(q: number) {\n this.q = q;\n }\n\n /**\n * Adds text to the index, extracting q-grams and building postings.\n *\n * @param page - Page number or identifier for this text\n * @param text - Text content to index\n * @param seam - Whether this text represents a seam (cross-page boundary)\n */\n addText(page: number, text: string, seam: boolean): void {\n const q = this.q;\n const m = text.length;\n if (m < q) {\n return;\n }\n\n for (let i = 0; i + q <= m; i++) {\n const gram = text.slice(i, i + q);\n\n // postings\n let postings = this.map.get(gram);\n if (!postings) {\n postings = [];\n this.map.set(gram, postings);\n }\n postings.push({ page, pos: i, seam });\n\n // freq\n this.gramFreq.set(gram, (this.gramFreq.get(gram) ?? 0) + 1);\n }\n }\n\n /**\n * Picks the rarest grams from an excerpt that exist in the index.\n */\n pickRare(excerpt: string, gramsPerExcerpt: number): { gram: string; offset: number }[] {\n gramsPerExcerpt = Math.max(1, Math.floor(gramsPerExcerpt));\n\n // extract unique grams with freqs (single pass)\n const items: GramItem[] = [];\n const seen = new Set<string>();\n const q = this.q;\n for (let i = 0; i + q <= excerpt.length; i++) {\n const gram = excerpt.slice(i, i + q);\n if (seen.has(gram)) {\n continue;\n }\n seen.add(gram);\n const freq = this.gramFreq.get(gram) ?? 0x7fffffff;\n items.push({ freq, gram, offset: i });\n }\n items.sort((a, b) => a.freq - b.freq);\n\n // prefer rare grams that exist; fallback to common ones if nothing exists\n const result: GramBase[] = [];\n for (const it of items) {\n if (this.map.has(it.gram)) {\n result.push({ gram: it.gram, offset: it.offset });\n if (result.length >= gramsPerExcerpt) {\n return result;\n }\n }\n }\n if (result.length < gramsPerExcerpt) {\n const chosen = new Set(result.map((r) => r.gram));\n for (let i = items.length - 1; i >= 0 && result.length < gramsPerExcerpt; i--) {\n const it = items[i]!;\n if (this.map.has(it.gram) && !chosen.has(it.gram)) {\n result.push({ gram: it.gram, offset: it.offset });\n chosen.add(it.gram);\n }\n }\n }\n return result;\n }\n\n getPostings(gram: string): Posting[] | undefined {\n return this.map.get(gram);\n }\n}\n","import type { MatchPolicy } from './types';\nimport { buildAhoCorasick } from './utils/ahocorasick';\nimport { DEFAULT_POLICY } from './utils/constants';\nimport { buildBook, calculateFuzzyScore, deduplicateExcerpts, findExactMatches, posToPage } from './utils/fuzzyUtils';\nimport { QGramIndex } from './utils/qgram';\nimport { sanitizeArabic } from './utils/sanitize';\n\n/**\n * Represents a candidate match position for fuzzy matching.\n */\ntype Candidate = {\n /** Page number where the candidate match is found */\n page: number;\n /** Starting position within the page or seam */\n start: number;\n /** Whether this candidate is from a seam (cross-page boundary) */\n seam: boolean;\n};\n\n/**\n * Data structure for cross-page text seams used in fuzzy matching.\n */\ntype SeamData = {\n /** Combined text from adjacent page boundaries */\n text: string;\n /** Starting page number for this seam */\n startPage: number;\n};\n\n/**\n * Represents a fuzzy match result with quality score.\n */\ntype FuzzyMatch = {\n /** Page number where the match was found */\n page: number;\n /** Edit distance (lower is better) */\n dist: number;\n};\n\n/**\n * Represents a page hit with quality metrics for ranking matches.\n */\ntype PageHit = {\n /** Quality score (0-1, higher is better) */\n score: number;\n /** Whether this is an exact match */\n exact: boolean;\n /** Whether this hit came from a seam candidate */\n seam: boolean;\n};\n\n/**\n * Creates seam data for cross-page matching by combining text from adjacent page boundaries.\n * Seams help find matches that span across page breaks.\n *\n * @param pagesN - Array of normalized page texts\n * @param seamLen - Length of text to take from each page boundary\n * @returns Array of seam data structures\n */\nfunction createSeams(pagesN: string[], seamLen: number): SeamData[] {\n const seams: SeamData[] = [];\n for (let p = 0; p + 1 < pagesN.length; p++) {\n const left = pagesN[p].slice(-seamLen);\n const right = pagesN[p + 1].slice(0, seamLen);\n const text = `${left} ${right}`;\n seams.push({ startPage: p, text });\n }\n return seams;\n}\n\n/**\n * Builds Q-gram index for efficient fuzzy matching candidate generation.\n * The index contains both regular pages and cross-page seams.\n *\n * @param pagesN - Array of normalized page texts\n * @param seams - Array of seam data for cross-page matching\n * @param q - Length of q-grams to index\n * @returns Constructed Q-gram index\n */\nfunction buildQGramIndex(pagesN: string[], seams: SeamData[], q: number): QGramIndex {\n const qidx = new QGramIndex(q);\n\n for (let p = 0; p < pagesN.length; p++) {\n qidx.addText(p, pagesN[p], false);\n }\n\n for (let p = 0; p < seams.length; p++) {\n qidx.addText(p, seams[p].text, true);\n }\n\n return qidx;\n}\n\n/**\n * Generates fuzzy matching candidates using rare q-grams from the excerpt.\n * Uses frequency-based selection to find the most discriminative grams.\n *\n * @param excerpt - Text excerpt to find candidates for\n * @param qidx - Q-gram index containing page and seam data\n * @param cfg - Match policy configuration\n * @returns Array of candidate match positions\n */\nfunction generateCandidates(excerpt: string, qidx: QGramIndex, cfg: Required<MatchPolicy>) {\n const seeds = qidx.pickRare(excerpt, cfg.gramsPerExcerpt);\n if (seeds.length === 0) {\n return [];\n }\n\n const candidates: Candidate[] = [];\n const seenKeys = new Set<string>();\n const excerptLen = excerpt.length;\n\n outer: for (const { gram, offset } of seeds) {\n const posts = qidx.getPostings(gram);\n if (!posts) {\n continue;\n }\n\n for (const p of posts) {\n const startPos = p.pos - offset;\n if (startPos < -Math.floor(excerptLen * 0.25)) {\n continue;\n }\n\n const start = Math.max(0, startPos);\n const key = `${p.page}:${start}:${p.seam ? 1 : 0}`;\n if (seenKeys.has(key)) {\n continue;\n }\n\n candidates.push({ page: p.page, seam: p.seam, start });\n seenKeys.add(key);\n\n if (candidates.length >= cfg.maxCandidatesPerExcerpt) {\n break outer;\n }\n }\n }\n\n return candidates;\n}\n\n/**\n * Finds the best fuzzy match among candidates by comparing edit distances.\n * Prioritizes lower edit distance, then earlier page number for tie-breaking.\n *\n * @param excerpt - Text excerpt to match\n * @param candidates - Array of candidate positions to evaluate\n * @param pagesN - Array of normalized page texts\n * @param seams - Array of seam data\n * @param cfg - Match policy configuration\n * @returns Best fuzzy match or null if none found\n */\nfunction findBestFuzzyMatch(\n excerpt: string,\n candidates: Candidate[],\n pagesN: string[],\n seams: SeamData[],\n cfg: Required<MatchPolicy>,\n): FuzzyMatch | null {\n if (excerpt.length === 0) {\n return null;\n }\n\n const maxDist = calculateMaxDistance(excerpt, cfg);\n cfg.log('maxDist', maxDist);\n\n const keyset = new Set<string>();\n let best: FuzzyMatch | null = null;\n\n for (const candidate of candidates) {\n if (shouldSkipCandidate(candidate, keyset)) {\n continue;\n }\n\n const match = evaluateCandidate(candidate, excerpt, pagesN, seams, maxDist, cfg);\n if (!match) {\n continue;\n }\n\n best = updateBestMatch(best, match, candidate);\n cfg.log('findBest best', best);\n\n if (match.dist === 0) {\n break;\n }\n }\n\n return best;\n}\n\n/**\n * Calculates the maximum edit distance allowed for a fuzzy comparison.\n *\n * @param excerpt - The excerpt currently being matched.\n * @param cfg - The resolved matching policy in effect.\n * @returns The maximum permitted edit distance for the excerpt.\n */\nfunction calculateMaxDistance(excerpt: string, cfg: Required<MatchPolicy>): number {\n return Math.max(cfg.maxEditAbs, Math.ceil(cfg.maxEditRel * excerpt.length));\n}\n\n/**\n * Checks whether a candidate has already been processed and should be skipped.\n *\n * @param candidate - The candidate under consideration.\n * @param keyset - A set of serialized candidate keys used for deduplication.\n * @returns True if the candidate was seen before, otherwise false.\n */\nfunction shouldSkipCandidate(candidate: Candidate, keyset: Set<string>): boolean {\n const key = `${candidate.page}:${candidate.start}:${candidate.seam ? 1 : 0}`;\n if (keyset.has(key)) {\n return true;\n }\n keyset.add(key);\n return false;\n}\n\n/**\n * Evaluates a candidate by computing its fuzzy score and checking acceptance.\n *\n * @param candidate - The candidate segment to evaluate.\n * @param excerpt - The normalized excerpt being matched.\n * @param pagesN - Normalized page content collection.\n * @param seams - Precomputed seam data for cross-page matching.\n * @param maxDist - Maximum allowed edit distance for this excerpt.\n * @param cfg - The resolved matching policy.\n * @returns The candidate's distance and acceptance threshold if valid, otherwise null.\n */\nfunction evaluateCandidate(\n candidate: Candidate,\n excerpt: string,\n pagesN: string[],\n seams: SeamData[],\n maxDist: number,\n cfg: Required<MatchPolicy>,\n): { dist: number; acceptance: number } | null {\n const res = calculateFuzzyScore(excerpt, candidate, pagesN, seams, maxDist);\n const dist = res?.dist ?? null;\n const acceptance = res?.acceptance ?? maxDist;\n\n cfg.log('dist', dist);\n\n return isValidMatch(dist, acceptance) ? { acceptance, dist: dist! } : null;\n}\n\n/**\n * Determines whether an evaluated match satisfies its acceptance threshold.\n *\n * @param dist - The computed edit distance for the match.\n * @param acceptance - The maximum acceptable distance for the match.\n * @returns True when the match should be accepted.\n */\nfunction isValidMatch(dist: number | null, acceptance: number): boolean {\n return dist !== null && dist <= acceptance;\n}\n\n/**\n * Updates the running \"best\" match if the current candidate improves it.\n *\n * @param current - The previously best fuzzy match, if any.\n * @param match - The latest candidate match metrics.\n * @param candidate - The candidate metadata associated with {@link match}.\n * @returns The preferred match after considering the candidate.\n */\nfunction updateBestMatch(\n current: FuzzyMatch | null,\n match: { dist: number; acceptance: number },\n candidate: Candidate,\n): FuzzyMatch {\n const newMatch = { dist: match.dist, page: candidate.page };\n\n if (!current) {\n return newMatch;\n }\n\n return isBetterMatch(match.dist, candidate.page, current.dist, current.page) ? newMatch : current;\n}\n\n/**\n * Determines whether a new match outranks the current best match.\n *\n * @param newDist - Edit distance for the new match.\n * @param newPage - Page index where the new match resides.\n * @param bestDist - Edit distance of the existing best match.\n * @param bestPage - Page index of the existing best match.\n * @returns True if the new match should replace the current best match.\n */\nfunction isBetterMatch(newDist: number, newPage: number, bestDist: number, bestPage: number): boolean {\n return newDist < bestDist || (newDist === bestDist && newPage < bestPage);\n}\n\n/**\n * Performs fuzzy matching for excerpts that didn't have exact matches.\n * Uses Q-gram indexing and bounded Levenshtein distance for efficiency.\n *\n * @param excerptsN - Array of normalized excerpts\n * @param pagesN - Array of normalized page texts\n * @param seenExact - Flags indicating which excerpts had exact matches\n * @param result - Result array to update with fuzzy match pages\n * @param cfg - Match policy configuration\n */\nfunction performFuzzyMatching(\n excerptsN: string[],\n pagesN: string[],\n seenExact: Uint8Array,\n result: Int32Array,\n cfg: Required<MatchPolicy>,\n): void {\n if (!cfg.enableFuzzy) {\n return;\n }\n\n const seams = createSeams(pagesN, cfg.seamLen);\n const qidx = buildQGramIndex(pagesN, seams, cfg.q);\n\n for (let i = 0; i < excerptsN.length; i++) {\n if (seenExact[i]) {\n continue;\n }\n\n const excerpt = excerptsN[i];\n cfg.log('excerpt', excerpt);\n if (!excerpt || excerpt.length < cfg.q) {\n continue;\n }\n\n const candidates = generateCandidates(excerpt, qidx, cfg);\n cfg.log('candidates', candidates);\n if (candidates.length === 0) {\n continue;\n }\n\n const best = findBestFuzzyMatch(excerpt, candidates, pagesN, seams, cfg);\n cfg.log('best', best);\n if (best) {\n result[i] = best.page;\n seenExact[i] = 1;\n }\n }\n}\n\n/**\n * Main function to find the single best match per excerpt.\n * Combines exact matching with fuzzy matching for comprehensive text search.\n *\n * @param pages - Array of page texts to search within\n * @param excerpts - Array of text excerpts to find matches for\n * @param policy - Optional matching policy configuration\n * @returns Array of page indices (one per excerpt, -1 if no match found)\n *\n * @example\n * ```typescript\n * const pages = ['Hello world', 'Goodbye world'];\n * const excerpts = ['Hello', 'Good bye']; // Note the typo\n * const matches = findMatches(pages, excerpts, { enableFuzzy: true });\n * // Returns [0, 1] - exact match on page 0, fuzzy match on page 1\n * ```\n */\nexport function findMatches(pages: string[], excerpts: string[], policy: MatchPolicy = {}) {\n const cfg = { ...DEFAULT_POLICY, ...policy };\n\n const pagesN = pages.map((p) => sanitizeArabic(p, 'aggressive'));\n const excerptsN = excerpts.map((e) => sanitizeArabic(e, 'aggressive'));\n\n if (policy.log) {\n policy.log('pages', pages);\n policy.log('excerpts', excerpts);\n policy.log('pagesN', pagesN);\n policy.log('excerptsN', excerptsN);\n }\n\n const { patIdToOrigIdxs, patterns } = deduplicateExcerpts(excerptsN);\n const { book, starts: pageStarts } = buildBook(pagesN);\n\n const { result, seenExact } = findExactMatches(book, pageStarts, patterns, patIdToOrigIdxs, excerpts.length);\n\n if (policy.log) {\n policy.log('findExactMatches result', result);\n policy.log('seenExact', seenExact);\n }\n\n if (!seenExact.every((seen) => seen === 1)) {\n performFuzzyMatching(excerptsN, pagesN, seenExact, result, cfg);\n }\n\n if (policy.log) {\n policy.log('performFuzzyMatching result', result);\n }\n\n return Array.from(result);\n}\n\n/**\n * Records exact matches for the findMatchesAll function.\n * Updates the hits tracking structure with exact match information.\n *\n * @param book - Concatenated text from all pages\n * @param pageStarts - Array of starting positions for each page\n * @param patterns - Array of deduplicated patterns to search for\n * @param patIdToOrigIdxs - Mapping from pattern IDs to original excerpt indices\n * @param hitsByExcerpt - Array of maps tracking hits per excerpt\n */\nfunction recordExactMatches(\n book: string,\n pageStarts: number[],\n patterns: string[],\n patIdToOrigIdxs: number[][],\n hitsByExcerpt: Array<Map<number, PageHit>>,\n): void {\n const ac = buildAhoCorasick(patterns);\n\n ac.find(book, (pid, endPos) => {\n const pat = patterns[pid];\n const startPos = endPos - pat.length;\n const startPage = posToPage(startPos, pageStarts);\n\n for (const origIdx of patIdToOrigIdxs[pid]) {\n const hits = hitsByExcerpt[origIdx];\n const prev = hits.get(startPage);\n if (!prev || !prev.exact) {\n hits.set(startPage, { exact: true, score: 1, seam: false });\n }\n }\n });\n}\n\n/**\n * Processes a single fuzzy candidate and updates hits if a better match is found.\n * Used internally by the findMatchesAll function for comprehensive matching.\n *\n * @param candidate - Candidate position to evaluate\n * @param excerpt - Text excerpt being matched\n * @param pagesN - Array of normalized page texts\n * @param seams - Array of seam data\n * @param maxDist - Maximum edit distance threshold\n * @param hits - Map of page hits to update\n * @param keyset - Set to track processed candidates (for deduplication)\n */\nfunction processFuzzyCandidate(\n candidate: Candidate,\n excerpt: string,\n pagesN: string[],\n seams: SeamData[],\n maxDist: number,\n hits: Map<number, PageHit>,\n keyset: Set<string>,\n): void {\n const key = `${candidate.page}:${candidate.start}:${candidate.seam ? 1 : 0}`;\n if (keyset.has(key)) {\n return;\n }\n keyset.add(key);\n\n const res = calculateFuzzyScore(excerpt, candidate, pagesN, seams, maxDist);\n if (!res) {\n return;\n }\n\n const { dist, acceptance } = res;\n if (dist > acceptance) {\n return;\n }\n\n const score = 1 - dist / acceptance;\n\n const entry = hits.get(candidate.page);\n if (!entry || (!entry.exact && score > entry.score)) {\n hits.set(candidate.page, { exact: false, score, seam: candidate.seam });\n }\n}\n\n/**\n * Processes fuzzy matching for a single excerpt in the findMatchesAll function.\n * Generates candidates and evaluates them for potential matches.\n *\n * @param excerptIndex - Index of the excerpt being processed\n * @param excerpt - Text excerpt to find matches for\n * @param pagesN - Array of normalized page texts\n * @param seams - Array of seam data\n * @param qidx - Q-gram index for candidate generation\n * @param hitsByExcerpt - Array of maps tracking hits per excerpt\n * @param cfg - Match policy configuration\n */\nfunction processSingleExcerptFuzzy(\n excerptIndex: number,\n excerpt: string,\n pagesN: string[],\n seams: SeamData[],\n qidx: QGramIndex,\n hitsByExcerpt: Array<Map<number, PageHit>>,\n cfg: Required<MatchPolicy>,\n): void {\n // Skip if we already have exact hits\n const hasExactHits = Array.from(hitsByExcerpt[excerptIndex].values()).some((v) => v.exact);\n if (hasExactHits) {\n return;\n }\n\n if (!excerpt || excerpt.length < cfg.q) {\n return;\n }\n\n const candidates = generateCandidates(excerpt, qidx, cfg);\n if (candidates.length === 0) {\n return;\n }\n\n const maxDist = Math.max(cfg.maxEditAbs, Math.ceil(cfg.maxEditRel * excerpt.length));\n const keyset = new Set<string>();\n const hits = hitsByExcerpt[excerptIndex];\n\n for (const candidate of candidates) {\n processFuzzyCandidate(candidate, excerpt, pagesN, seams, maxDist, hits, keyset);\n }\n}\n\n/**\n * Records fuzzy matches for excerpts that don't have exact matches.\n * Used by findMatchesAll to provide comprehensive matching results.\n *\n * @param excerptsN - Array of normalized excerpts\n * @param pagesN - Array of normalized page texts\n * @param hitsByExcerpt - Array of maps tracking hits per excerpt\n * @param cfg - Match policy configuration\n */\nfunction recordFuzzyMatches(\n excerptsN: string[],\n pagesN: string[],\n hitsByExcerpt: Array<Map<number, PageHit>>,\n cfg: Required<MatchPolicy>,\n): void {\n const seams = createSeams(pagesN, cfg.seamLen);\n const qidx = buildQGramIndex(pagesN, seams, cfg.q);\n\n for (let i = 0; i < excerptsN.length; i++) {\n processSingleExcerptFuzzy(i, excerptsN[i], pagesN, seams, qidx, hitsByExcerpt, cfg);\n }\n}\n\n/**\n * Sorts matches by quality and page order for optimal ranking.\n * Exact matches are prioritized over fuzzy matches, with secondary sorting by page order.\n *\n * @param hits - Map of page hits with quality scores\n * @returns Array of page numbers sorted by match quality\n */\nconst sortMatches = (hits: Map<number, PageHit>) => {\n if (hits.size === 0) {\n return [];\n }\n\n // 1) Collapse adjacent seam pairs: keep the stronger seam and drop the weaker neighbor.\n collapseAdjacentSeams(hits);\n\n // 2) Remove seam hits that are worse than a non-seam neighbor on the following page.\n removeWeakSeams(hits);\n\n // 3) Split and rank: exact first in reading order; then fuzzy by score desc, then page asc.\n return rankHits(hits);\n};\n\n/**\n * Removes weaker seam matches from adjacent seam pairs.\n *\n * @param hits - Mutable map of page hits that may contain seam entries.\n */\nconst collapseAdjacentSeams = (hits: Map<number, PageHit>) => {\n const pagesAsc = Array.from(hits.keys()).sort((a, b) => a - b);\n\n for (const page of pagesAsc) {\n const currentHit = hits.get(page);\n const nextHit = hits.get(page + 1);\n\n if (shouldCollapseSeams(currentHit, nextHit)) {\n const pageToRemove = selectWeakerSeam(page, currentHit!, nextHit!);\n hits.delete(pageToRemove);\n }\n }\n};\n\n/**\n * Checks whether two neighboring hits are both seams that should be merged.\n *\n * @param hit1 - The first hit in the pair.\n * @param hit2 - The second hit in the pair.\n * @returns True if both hits represent seam matches.\n */\nconst shouldCollapseSeams = (hit1?: PageHit, hit2?: PageHit): boolean => {\n return Boolean(hit1?.seam && hit2?.seam);\n};\n\n/**\n * Selects which seam page to discard based on score ordering.\n *\n * @param page1 - The page index for the first seam hit.\n * @param hit1 - The first seam hit entry.\n * @param hit2 - The second seam hit entry on the following page.\n * @returns The page index that should be removed from the hits map.\n */\nconst selectWeakerSeam = (page1: number, hit1: PageHit, hit2: PageHit): number => {\n if (hit2.score > hit1.score) {\n return page1;\n }\n if (hit2.score < hit1.score) {\n return page1 + 1;\n }\n return page1 + 1; // Tie: prefer earlier page\n};\n\n/**\n * Removes seam hits that are redundant compared to their neighbors.\n *\n * @param hits - Mutable map of page hits that may contain redundant seams.\n */\nconst removeWeakSeams = (hits: Map<number, PageHit>) => {\n const seamPages = Array.from(hits.entries())\n .filter(([, hit]) => hit.seam)\n .map(([page]) => page);\n\n for (const page of seamPages) {\n const seamHit = hits.get(page)!;\n const neighbor = hits.get(page + 1);\n\n if (isSeamRedundant(seamHit, neighbor)) {\n hits.delete(page);\n }\n }\n};\n\n/**\n * Determines whether a seam hit is redundant when compared to its neighbor.\n *\n * @param seamHit - The seam hit currently under review.\n * @param neighbor - The neighboring hit on the following page, if any.\n * @returns True if the seam hit should be removed.\n */\nconst isSeamRedundant = (seamHit: PageHit, neighbor?: PageHit): boolean => {\n if (!neighbor) {\n return false;\n }\n return neighbor.exact || (!neighbor.seam && neighbor.score >= seamHit.score);\n};\n\n/**\n * Splits hits into exact and fuzzy categories, then sorts and merges them.\n *\n * @param hits - Map of page hits to rank.\n * @returns Sorted page numbers ordered by relevance.\n */\nconst rankHits = (hits: Map<number, PageHit>): number[] => {\n const exact: [number, PageHit][] = [];\n const fuzzy: [number, PageHit][] = [];\n\n for (const entry of hits.entries()) {\n if (entry[1].exact) {\n exact.push(entry);\n } else {\n fuzzy.push(entry);\n }\n }\n\n exact.sort((a, b) => a[0] - b[0]);\n fuzzy.sort((a, b) => b[1].score - a[1].score || a[0] - b[0]);\n\n return [...exact, ...fuzzy].map((entry) => entry[0]);\n};\n\n/**\n * Main function to find all matches per excerpt, ranked by quality.\n * Returns comprehensive results with both exact and fuzzy matches for each excerpt.\n *\n * @param pages - Array of page texts to search within\n * @param excerpts - Array of text excerpts to find matches for\n * @param policy - Optional matching policy configuration\n * @returns Array of page index arrays (one array per excerpt, sorted by match quality)\n *\n * @example\n * ```typescript\n * const pages = ['Hello world', 'Hello there', 'Goodbye world'];\n * const excerpts = ['Hello'];\n * const matches = findMatchesAll(pages, excerpts);\n * // Returns [[0, 1]] - both pages 0 and 1 contain \"Hello\", sorted by page order\n * ```\n */\nexport function findMatchesAll(pages: string[], excerpts: string[], policy: MatchPolicy = {}): number[][] {\n const cfg = { ...DEFAULT_POLICY, ...policy };\n\n const pagesN = pages.map((p) => sanitizeArabic(p, 'aggressive'));\n const excerptsN = excerpts.map((e) => sanitizeArabic(e, 'aggressive'));\n\n if (policy.log) {\n policy.log('pages', pages);\n policy.log('excerpts', excerpts);\n policy.log('pagesN', pagesN);\n policy.log('excerptsN', excerptsN);\n }\n\n const { patIdToOrigIdxs, patterns } = deduplicateExcerpts(excerptsN);\n const { book, starts: pageStarts } = buildBook(pagesN);\n\n // Initialize hit tracking\n const hitsByExcerpt: Array<Map<number, PageHit>> = Array.from({ length: excerpts.length }, () => new Map());\n\n // Record exact matches\n recordExactMatches(book, pageStarts, patterns, patIdToOrigIdxs, hitsByExcerpt);\n\n // Record fuzzy matches if enabled\n if (cfg.enableFuzzy) {\n recordFuzzyMatches(excerptsN, pagesN, hitsByExcerpt, cfg);\n }\n\n // Sort and return results\n return hitsByExcerpt.map((hits) => sortMatches(hits));\n}\n","import { PATTERNS } from './utils/textUtils';\n\n/**\n * Character statistics for analyzing text content and patterns\n */\ntype CharacterStats = {\n /** Number of Arabic script characters in the text */\n arabicCount: number;\n /** Map of character frequencies for repetition analysis */\n charFreq: Map<string, number>;\n /** Number of digit characters (0-9) in the text */\n digitCount: number;\n /** Number of Latin alphabet characters (a-z, A-Z) in the text */\n latinCount: number;\n /** Number of punctuation characters in the text */\n punctuationCount: number;\n /** Number of whitespace characters in the text */\n spaceCount: number;\n /** Number of symbol characters (non-alphanumeric, non-punctuation) in the text */\n symbolCount: number;\n};\n\n/**\n * Determines if a given Arabic text string is likely to be noise or unwanted OCR artifacts.\n * This function performs comprehensive analysis to identify patterns commonly associated\n * with OCR errors, formatting artifacts, or meaningless content in Arabic text processing.\n *\n * @param text - The input string to analyze for noise patterns\n * @returns true if the text is likely noise or unwanted content, false if it appears to be valid Arabic content\n *\n * @example\n * ```typescript\n * import { isArabicTextNoise } from 'baburchi';\n *\n * console.log(isArabicTextNoise('---')); // true (formatting artifact)\n * console.log(isArabicTextNoise('ุงูุณูุงู
ุนูููู
')); // false (valid Arabic)\n * console.log(isArabicTextNoise('ABC')); // true (uppercase pattern)\n * ```\n */\nexport const isArabicTextNoise = (text: string): boolean => {\n // Early return for empty or very short strings\n if (!text || text.trim().length === 0) {\n return true;\n }\n\n const trimmed = text.trim();\n const length = trimmed.length;\n\n // Very short strings are likely noise unless they're meaningful Arabic\n if (length < 2) {\n return true;\n }\n\n // Check for basic noise patterns first\n if (isBasicNoisePattern(trimmed)) {\n return true;\n }\n\n const charStats = analyzeCharacterStats(trimmed);\n\n // Check for excessive repetition\n if (hasExcessiveRepetition(charStats, length)) {\n return true;\n }\n\n // Check if text contains Arabic characters\n const hasArabic = PATTERNS.arabicCharacters.test(trimmed);\n\n // Handle non-Arabic text\n if (!hasArabic && /[a-zA-Z]/.test(trimmed)) {\n return true;\n }\n\n // Arabic-specific validation\n if (hasArabic) {\n return !isValidArabicContent(charStats, length);\n }\n\n // Non-Arabic content validation\n return isNonArabicNoise(charStats, length, trimmed);\n};\n\n/**\n * Analyzes character composition and frequency statistics for the input text.\n * Categorizes characters by type (Arabic, Latin, digits, spaces, punctuation, symbols)\n * and tracks character frequency for pattern analysis.\n *\n * @param text - The text string to analyze\n * @returns CharacterStats object containing detailed character analysis\n *\n * @example\n * ```typescript\n * import { analyzeCharacterStats } from 'baburchi';\n *\n * const stats = analyzeCharacterStats('ู
ุฑุญุจุง 123!');\n * console.log(stats.arabicCount); // 5\n * console.log(stats.digitCount); // 3\n * console.log(stats.symbolCount); // 1\n * ```\n */\nexport function analyzeCharacterStats(text: string): CharacterStats {\n const stats: CharacterStats = {\n arabicCount: 0,\n charFreq: new Map<string, number>(),\n digitCount: 0,\n latinCount: 0,\n punctuationCount: 0,\n spaceCount: 0,\n symbolCount: 0,\n };\n\n const chars = Array.from(text);\n\n for (const char of chars) {\n // Count character frequency for repetition detection\n stats.charFreq.set(char, (stats.charFreq.get(char) || 0) + 1);\n\n if (PATTERNS.arabicCharacters.test(char)) {\n stats.arabicCount++;\n } else if (/\\d/.test(char)) {\n stats.digitCount++;\n } else if (/[a-zA-Z]/.test(char)) {\n stats.latinCount++;\n } else if (/\\s/.test(char)) {\n stats.spaceCount++;\n } else if (/[.,;:()[\\]{}\"\"\"''`]/.test(char)) {\n stats.punctuationCount++;\n } else {\n stats.symbolCount++;\n }\n }\n\n return stats;\n}\n\n/**\n * Detects excessive repetition of specific characters that commonly indicate noise.\n * Focuses on repetitive characters like exclamation marks, dots, dashes, equals signs,\n * and underscores that often appear in OCR artifacts or formatting elements.\n *\n * @param charStats - Character statistics from analyzeCharacterStats\n * @param textLength - Total length of the original text\n * @returns true if excessive repetition is detected, false otherwise\n *\n * @example\n * ```typescript\n * import { hasExcessiveRepetition, analyzeCharacterStats } from 'baburchi';\n *\n * const stats = analyzeCharacterStats('!!!!!');\n * console.log(hasExcessiveRepetition(stats, 5)); // true\n *\n * const normalStats = analyzeCharacterStats('hello world');\n * console.log(hasExcessiveRepetition(normalStats, 11)); // false\n * ```\n */\nexport function hasExcessiveRepetition(charStats: CharacterStats, textLength: number): boolean {\n let repeatCount = 0;\n const repetitiveChars = ['!', '.', '-', '=', '_'];\n\n for (const [char, count] of charStats.charFreq) {\n if (count >= 5 && repetitiveChars.includes(char)) {\n repeatCount += count;\n }\n }\n\n // High repetition ratio indicates noise\n return repeatCount / textLength > 0.4;\n}\n\n/**\n * Identifies text that matches common noise patterns using regular expressions.\n * Detects patterns like repeated dashes, dot sequences, uppercase-only text,\n * digit-dash combinations, and other formatting artifacts commonly found in OCR output.\n *\n * @param text - The text string to check against noise patterns\n * @returns true if the text matches a basic noise pattern, false otherwise\n *\n * @example\n * ```typescript\n * import { isBasicNoisePattern } from 'baburchi';\n *\n * console.log(isBasicNoisePattern('---')); // true\n * console.log(isBasicNoisePattern('...')); // true\n * console.log(isBasicNoisePattern('ABC')); // true\n * console.log(isBasicNoisePattern('- 77')); // true\n * console.log(isBasicNoisePattern('hello world')); // false\n * ```\n */\nexport function isBasicNoisePattern(text: string): boolean {\n const noisePatterns = [\n /^[-=_โโบโป\\s]*$/, // Only dashes, equals, underscores, special chars, or spaces\n /^[.\\s]*$/, // Only dots and spaces\n /^[!\\s]*$/, // Only exclamation marks and spaces\n /^[A-Z\\s]*$/, // Only uppercase letters and spaces (like \"Ap Ap Ap\")\n /^[-\\d\\s]*$/, // Only dashes, digits and spaces (like \"- 77\", \"- 4\")\n /^\\d+\\s*$/, // Only digits and spaces (like \"1\", \" 1 \")\n /^[A-Z]\\s*$/, // Single uppercase letter with optional spaces\n /^[โ\\s]*$/, // Only em-dashes and spaces\n /^[เฅเคฐ\\s-]*$/, // Devanagari characters (likely OCR errors)\n ];\n\n return noisePatterns.some((pattern) => pattern.test(text));\n}\n\n/**\n * Determines if non-Arabic content should be classified as noise based on various heuristics.\n * Analyzes symbol-to-content ratios, text length, spacing patterns, and content composition\n * to identify unwanted OCR artifacts or meaningless content.\n *\n * @param charStats - Character statistics from analyzeCharacterStats\n * @param textLength - Total length of the original text\n * @param text - The original text string for additional pattern matching\n * @returns true if the content is likely noise, false if it appears to be valid content\n *\n * @example\n * ```typescript\n * import { isNonArabicNoise, analyzeCharacterStats } from 'baburchi';\n *\n * const stats = analyzeCharacterStats('!!!');\n * console.log(isNonArabicNoise(stats, 3, '!!!')); // true\n *\n * const validStats = analyzeCharacterStats('2023');\n * console.log(isNonArabicNoise(validStats, 4, '2023')); // false\n * ```\n */\nexport function isNonArabicNoise(charStats: CharacterStats, textLength: number, text: string): boolean {\n const contentChars = charStats.arabicCount + charStats.latinCount + charStats.digitCount;\n\n // Text that's mostly symbols or punctuation is likely noise\n if (contentChars === 0) {\n return true;\n }\n\n // Check for specific spacing patterns that indicate noise\n if (isSpacingNoise(charStats, contentChars, textLength)) {\n return true;\n }\n\n // Special handling for Arabic numerals in parentheses (like \"(ูฆู ูกู ).\")\n const hasArabicNumerals = /[ู -ูฉ]/.test(text);\n if (hasArabicNumerals && charStats.digitCount >= 3) {\n return false;\n }\n\n // High symbol-to-content ratio indicates noise, but be more lenient with punctuation\n // Allow more punctuation for valid content like references, citations, etc.\n const adjustedNonContentChars = charStats.symbolCount + Math.max(0, charStats.punctuationCount - 5);\n if (adjustedNonContentChars / Math.max(contentChars, 1) > 2) {\n return true;\n }\n\n // Very short strings with no Arabic are likely noise (except substantial numbers)\n if (textLength <= 5 && charStats.arabicCount === 0 && !(/^\\d+$/.test(text) && charStats.digitCount >= 3)) {\n return true;\n }\n\n // Allow pure numbers if they're substantial (like years)\n if (/^\\d{3,4}$/.test(text)) {\n return false;\n }\n\n // Default to not noise for longer content\n return textLength <= 10;\n}\n\n/**\n * Detects problematic spacing patterns that indicate noise or OCR artifacts.\n * Identifies cases where spacing is excessive relative to content, or where\n * single characters are surrounded by spaces in a way that suggests OCR errors.\n *\n * @param charStats - Character statistics from analyzeCharacterStats\n * @param contentChars - Number of meaningful content characters (Arabic + Latin + digits)\n * @param textLength - Total length of the original text\n * @returns true if spacing patterns indicate noise, false otherwise\n *\n * @example\n * ```typescript\n * import { isSpacingNoise, analyzeCharacterStats } from 'baburchi';\n *\n * const stats = analyzeCharacterStats(' a ');\n * const contentChars = stats.arabicCount + stats.latinCount + stats.digitCount;\n * console.log(isSpacingNoise(stats, contentChars, 3)); // true\n *\n * const normalStats = analyzeCharacterStats('hello world');\n * const normalContent = normalStats.arabicCount + normalStats.latinCount + normalStats.digitCount;\n * console.log(isSpacingNoise(normalStats, normalContent, 11)); // false\n * ```\n */\nexport function isSpacingNoise(charStats: CharacterStats, contentChars: number, textLength: number): boolean {\n const { arabicCount, spaceCount } = charStats;\n\n // Too many spaces relative to content\n if (spaceCount > 0 && contentChars === spaceCount + 1 && contentChars <= 5) {\n return true;\n }\n\n // Short text with multiple spaces and no Arabic\n if (textLength <= 10 && spaceCount >= 2 && arabicCount === 0) {\n return true;\n }\n\n // Excessive spacing ratio\n if (spaceCount / textLength > 0.6) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Validates whether Arabic content is substantial enough to be considered meaningful.\n * Uses character counts and text length to determine if Arabic text contains\n * sufficient content or if it's likely to be a fragment or OCR artifact.\n *\n * @param charStats - Character statistics from analyzeCharacterStats\n * @param textLength - Total length of the original text\n * @returns true if the Arabic content appears valid, false if it's likely noise\n *\n * @example\n * ```typescript\n * import { isValidArabicContent, analyzeCharacterStats } from 'baburchi';\n *\n * const validStats = analyzeCharacterStats('ุงูุณูุงู
ุนูููู
');\n * console.log(isValidArabicContent(validStats, 12)); // true\n *\n * const shortStats = analyzeCharacterStats('ุต');\n * console.log(isValidArabicContent(shortStats, 1)); // false\n *\n * const withDigitsStats = analyzeCharacterStats('ุต 5');\n * console.log(isValidArabicContent(withDigitsStats, 3)); // true\n * ```\n */\nexport function isValidArabicContent(charStats: CharacterStats, textLength: number): boolean {\n // Arabic text with reasonable content length is likely valid\n if (charStats.arabicCount >= 3) {\n return true;\n }\n\n // Short Arabic snippets with numbers might be valid (like dates, references)\n if (charStats.arabicCount >= 1 && charStats.digitCount > 0 && textLength <= 20) {\n return true;\n }\n\n // Allow short Arabic words with punctuation (like \"ูู.\" - \"for him/it.\")\n if (charStats.arabicCount >= 2 && charStats.punctuationCount <= 2 && textLength <= 10) {\n return true;\n }\n\n // Allow single meaningful Arabic words that are common standalone terms\n // This handles cases like pronouns, prepositions, common short words\n if (charStats.arabicCount >= 1 && textLength <= 5 && charStats.punctuationCount <= 1) {\n return true;\n }\n\n return false;\n}\n","import type { FixTypoOptions } from './types';\nimport { sanitizeArabic } from './utils/sanitize';\nimport { alignTokenSequences, areSimilarAfterNormalization, calculateSimilarity } from './utils/similarity';\nimport {\n handleFootnoteFusion,\n handleFootnoteSelection,\n handleStandaloneFootnotes,\n tokenizeText,\n} from './utils/textUtils';\n\n/**\n * Selects the best token(s) from an aligned pair during typo correction.\n * Uses various heuristics including normalization, footnote handling, typo symbols,\n * and similarity scores to determine which token(s) to keep.\n *\n * @param originalToken - Token from the original OCR text (may be null)\n * @param altToken - Token from the alternative OCR text (may be null)\n * @param options - Configuration options including typo symbols and similarity threshold\n * @returns Array of selected tokens (usually contains one token, but may contain multiple)\n */\nconst selectBestTokens = (\n originalToken: null | string,\n altToken: null | string,\n { similarityThreshold, typoSymbols }: FixTypoOptions,\n): string[] => {\n // Handle missing tokens\n if (originalToken === null) {\n return [altToken!];\n }\n if (altToken === null) {\n return [originalToken];\n }\n\n // Preserve original if same after normalization (keeps diacritics)\n if (sanitizeArabic(originalToken) === sanitizeArabic(altToken)) {\n return [originalToken];\n }\n\n // Handle embedded footnotes\n const result = handleFootnoteSelection(originalToken, altToken);\n if (result) {\n return result;\n }\n\n // Handle standalone footnotes\n const footnoteResult = handleStandaloneFootnotes(originalToken, altToken);\n if (footnoteResult) {\n return footnoteResult;\n }\n\n // Handle typo symbols - prefer the symbol itself\n if (typoSymbols.includes(originalToken) || typoSymbols.includes(altToken)) {\n const typoSymbol = typoSymbols.find((symbol) => symbol === originalToken || symbol === altToken);\n return typoSymbol ? [typoSymbol] : [originalToken];\n }\n\n // Choose based on similarity\n const normalizedOriginal = sanitizeArabic(originalToken);\n const normalizedAlt = sanitizeArabic(altToken);\n const similarity = calculateSimilarity(normalizedOriginal, normalizedAlt);\n\n return [similarity > similarityThreshold ? originalToken : altToken];\n};\n\n/**\n * Removes duplicate tokens and handles footnote fusion in post-processing.\n * Identifies and removes tokens that are highly similar while preserving\n * important variations. Also handles special cases like footnote merging.\n *\n * @param tokens - Array of tokens to process\n * @param highSimilarityThreshold - Threshold for detecting duplicates (0.0 to 1.0)\n * @returns Array of tokens with duplicates removed and footnotes fused\n */\nconst removeDuplicateTokens = (tokens: string[], highSimilarityThreshold: number): string[] => {\n if (tokens.length === 0) {\n return tokens;\n }\n\n const result: string[] = [];\n\n for (const currentToken of tokens) {\n if (result.length === 0) {\n result.push(currentToken);\n continue;\n }\n\n const previousToken = result.at(-1)!;\n\n // Handle ordinary echoes (similar tokens)\n if (areSimilarAfterNormalization(previousToken, currentToken, highSimilarityThreshold)) {\n // Keep the shorter version\n if (currentToken.length < previousToken.length) {\n result[result.length - 1] = currentToken;\n }\n continue;\n }\n\n // Handle footnote fusion cases\n if (handleFootnoteFusion(result, previousToken, currentToken)) {\n continue;\n }\n\n result.push(currentToken);\n }\n\n return result;\n};\n\n/**\n * Processes text alignment between original and alternate OCR results to fix typos.\n * Uses the Needleman-Wunsch sequence alignment algorithm to align tokens,\n * then selects the best tokens and performs post-processing.\n *\n * @param originalText - Original OCR text that may contain typos\n * @param altText - Reference text from alternate OCR for comparison\n * @param options - Configuration options for alignment and selection\n * @returns Corrected text with typos fixed\n */\nexport const processTextAlignment = (originalText: string, altText: string, options: FixTypoOptions): string => {\n const originalTokens = tokenizeText(originalText, options.typoSymbols);\n const altTokens = tokenizeText(altText, options.typoSymbols);\n\n // Align token sequences\n const alignedPairs = alignTokenSequences(\n originalTokens,\n altTokens,\n options.typoSymbols,\n options.similarityThreshold,\n );\n\n // Select best tokens from each aligned pair\n const mergedTokens = alignedPairs.flatMap(([original, alt]) => selectBestTokens(original, alt, options));\n\n // Remove duplicates and handle post-processing\n const finalTokens = removeDuplicateTokens(mergedTokens, options.highSimilarityThreshold);\n\n return finalTokens.join(' ');\n};\n\n/**\n * Convenience wrapper around {@link processTextAlignment} that accepts partial options.\n *\n * @param original - The source text that may contain typographical errors.\n * @param correction - The reference text used to correct the {@link original} text.\n * @param options - Partial typo correction options combined with required typo symbols.\n * @returns The corrected text generated from the alignment process.\n */\nexport const fixTypo = (\n original: string,\n correction: string,\n {\n highSimilarityThreshold = 0.8,\n similarityThreshold = 0.6,\n typoSymbols,\n }: Partial<FixTypoOptions> & Pick<FixTypoOptions, 'typoSymbols'>,\n) => {\n return processTextAlignment(original, correction, { highSimilarityThreshold, similarityThreshold, typoSymbols });\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -5,15 +5,15 @@
|
|
|
5
5
|
},
|
|
6
6
|
"description": "A lightweight TypeScript library designed to fix typos in OCR post-processing.",
|
|
7
7
|
"devDependencies": {
|
|
8
|
-
"@biomejs/biome": "^2.3.
|
|
9
|
-
"@types/bun": "^1.3.
|
|
8
|
+
"@biomejs/biome": "^2.3.11",
|
|
9
|
+
"@types/bun": "^1.3.5",
|
|
10
10
|
"@types/node": "^25.0.3",
|
|
11
11
|
"semantic-release": "^25.0.2",
|
|
12
|
-
"tsdown": "^0.
|
|
12
|
+
"tsdown": "^0.19.0-beta.5",
|
|
13
13
|
"typescript": "^5.9.3"
|
|
14
14
|
},
|
|
15
15
|
"engines": {
|
|
16
|
-
"bun": ">=1.3.
|
|
16
|
+
"bun": ">=1.3.5",
|
|
17
17
|
"node": ">=24.0.0"
|
|
18
18
|
},
|
|
19
19
|
"exports": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"main": "dist/index.js",
|
|
41
41
|
"module": "dist/index.js",
|
|
42
42
|
"name": "baburchi",
|
|
43
|
+
"packageManager": "bun@1.3.5",
|
|
43
44
|
"repository": {
|
|
44
45
|
"type": "git",
|
|
45
46
|
"url": "git+https://github.com/ragaeeb/baburchi.git"
|
|
@@ -52,5 +53,5 @@
|
|
|
52
53
|
"source": "src/index.ts",
|
|
53
54
|
"type": "module",
|
|
54
55
|
"types": "dist/index.d.ts",
|
|
55
|
-
"version": "1.7.
|
|
56
|
+
"version": "1.7.3"
|
|
56
57
|
}
|