🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@stll/fuzzy-search

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@stll/fuzzy-search - npm Package Compare versions

Comparing version
0.0.2
to
0.1.1
+153
dist/index.d.mts
//#region src/core.d.ts
type NativeBinding = {
FuzzySearch: new (entries: NormalizedEntry[], options?: Options) => NativeFuzzySearch;
distance: (a: string, b: string, metric: Metric | null) => number;
};
type NativeFuzzySearch = {
patternCount: number;
isMatch(haystack: string): boolean;
_findIterPacked(haystack: string): Uint32Array;
replaceAll(haystack: string, replacements: string[]): string;
};
type NormalizedEntry = {
pattern: string;
distance?: number;
name?: string;
};
/** Set the native backend. Must be called once
* before any class constructor. */
/** Distance metric for fuzzy matching. */
type Metric = "levenshtein" | "damerau-levenshtein";
/** Options for constructing a `FuzzySearch`. */
type Options = {
/**
* Distance metric.
* - `"levenshtein"`: insertions, deletions,
* substitutions (default).
* - `"damerau-levenshtein"`: + transpositions
* of adjacent characters (ab -> ba = 1 edit).
* @default "levenshtein"
*/
metric?: Metric;
/**
* Strip diacritics before matching (NFD
* decompose + remove combining marks).
* "Pribram" matches "Pribram" at distance 0.
* @default false
*/
normalizeDiacritics?: boolean;
/**
* Use Unicode word boundaries (covers all
* scripts). CJK characters are treated as
* standalone words.
* @default true
*/
unicodeBoundaries?: boolean;
/**
* Only match whole words. Fuzzy matches on
* substrings are usually noise; require word
* boundaries unless opted out.
* @default true
*/
wholeWords?: boolean;
/**
* Case-insensitive matching (Unicode-aware).
* @default false
*/
caseInsensitive?: boolean;
};
/** A pattern entry with its edit distance. */
type PatternEntry = string | {
pattern: string;
/** Max edit distance. Must be less than
* pattern length. `"auto"` uses the
* Elasticsearch convention: 1-2 chars -> 0,
* 3-5 chars -> 1, 6+ chars -> 2.
* @default 1 */
distance?: number | "auto"; /** Optional name for the pattern. */
name?: string;
};
/** A single fuzzy match result. */
type FuzzyMatch = {
/** Index into the patterns array. */pattern: number;
/** Start UTF-16 code unit offset (compatible
* with `String.prototype.slice()`). */
start: number; /** End offset (exclusive). */
end: number;
/** The matched text
* (`haystack.slice(start, end)`). */
text: string; /** Actual Levenshtein edit distance. */
distance: number; /** Pattern name (if provided). */
name?: string;
};
/**
* Fuzzy string matcher. Finds approximate
* matches within edit distance k, immune to
* typos, OCR errors, and diacritics variants.
*
* Uses Myers' bit-parallel algorithm for O(n)
* scanning per pattern (patterns up to 64 chars).
*
* @throws {Error} If a pattern is empty, too
* long (> 64 chars), or distance > 3.
*
* @example
* ```ts
* const fs = new FuzzySearch([
* { pattern: "Gaislerova", distance: 1 },
* { pattern: "Novak", distance: 1 },
* ], {
* normalizeDiacritics: true,
* wholeWords: true,
* });
*
* fs.findIter("Gais1erova a Nowak");
* // [
* // { pattern: 0, start: 0, end: 10,
* // text: "Gais1erova", distance: 1 },
* // { pattern: 1, start: 13, end: 18,
* // text: "Nowak", distance: 1 },
* // ]
* ```
*/
declare class FuzzySearch {
private _names;
private _inner;
constructor(patterns: PatternEntry[], options?: Options);
/** Number of patterns in the matcher. */
get patternCount(): number;
/**
* Returns `true` if any pattern matches
* within its edit distance.
*/
isMatch(haystack: string): boolean;
/** Find all non-overlapping fuzzy matches. */
findIter(haystack: string): FuzzyMatch[];
/**
* Replace all fuzzy matches.
* `replacements[i]` replaces pattern `i`.
*
* @throws {Error} If `replacements.length`
* does not equal `patternCount`.
*/
replaceAll(haystack: string, replacements: string[]): string;
}
/**
* Compute edit distance between two strings.
*
* Uses Unicode characters (not UTF-16 code units),
* so emoji and supplementary plane characters are
* handled correctly.
*
* @example
* ```ts
* distance("Novak", "Nowak"); // 1
* distance("abcd", "abdc"); // 2
* distance("abcd", "abdc",
* "damerau-levenshtein"); // 1
* ```
*/
declare const distance: (a: string, b: string, metric?: Metric) => number;
//#endregion
export { type FuzzyMatch, FuzzySearch, type Metric, type NativeBinding, type Options, type PatternEntry, distance };
//# sourceMappingURL=index.d.mts.map
import { createRequire } from "node:module";
//#region src/core.ts
let binding;
/** Set the native backend. Must be called once
* before any class constructor. */
const initBinding = (b) => {
binding = b;
};
const resolveDistance = (dist, patternLength) => {
if (dist !== "auto") return dist;
if (patternLength <= 2) return 0;
if (patternLength <= 5) return 1;
return 2;
};
const normalizeEntry = (p, i) => {
if (typeof p === "string") return { pattern: p };
if (typeof p === "object" && p !== null && typeof p.pattern === "string") {
if (p.distance === "auto") return {
...p,
distance: resolveDistance("auto", p.pattern.length)
};
return p;
}
throw new TypeError(`Pattern at index ${i} must be a string or { pattern, distance?, name? }`);
};
const unpack = (packed, haystack, names) => {
const len = packed.length;
const matches = [];
for (let i = 0; i < len; i += 4) {
const idx = packed[i];
const start = packed[i + 1];
const end = packed[i + 2];
const distance = packed[i + 3];
if (idx === void 0 || start === void 0 || end === void 0 || distance === void 0) throw new Error(`Malformed packed array at offset ${String(i)}`);
const m = {
pattern: idx,
start,
end,
text: haystack.slice(start, end),
distance
};
if (names[idx] !== void 0) m.name = names[idx];
matches.push(m);
}
return matches;
};
/**
* Fuzzy string matcher. Finds approximate
* matches within edit distance k, immune to
* typos, OCR errors, and diacritics variants.
*
* Uses Myers' bit-parallel algorithm for O(n)
* scanning per pattern (patterns up to 64 chars).
*
* @throws {Error} If a pattern is empty, too
* long (> 64 chars), or distance > 3.
*
* @example
* ```ts
* const fs = new FuzzySearch([
* { pattern: "Gaislerova", distance: 1 },
* { pattern: "Novak", distance: 1 },
* ], {
* normalizeDiacritics: true,
* wholeWords: true,
* });
*
* fs.findIter("Gais1erova a Nowak");
* // [
* // { pattern: 0, start: 0, end: 10,
* // text: "Gais1erova", distance: 1 },
* // { pattern: 1, start: 13, end: 18,
* // text: "Nowak", distance: 1 },
* // ]
* ```
*/
var FuzzySearch = class {
_names;
_inner;
constructor(patterns, options) {
const entries = patterns.map(normalizeEntry);
this._names = entries.map((e) => e.name);
this._inner = new binding.FuzzySearch(entries, options);
}
/** Number of patterns in the matcher. */
get patternCount() {
return this._inner.patternCount;
}
/**
* Returns `true` if any pattern matches
* within its edit distance.
*/
isMatch(haystack) {
return this._inner.isMatch(haystack);
}
/** Find all non-overlapping fuzzy matches. */
findIter(haystack) {
return unpack(this._inner._findIterPacked(haystack), haystack, this._names);
}
/**
* Replace all fuzzy matches.
* `replacements[i]` replaces pattern `i`.
*
* @throws {Error} If `replacements.length`
* does not equal `patternCount`.
*/
replaceAll(haystack, replacements) {
return this._inner.replaceAll(haystack, replacements);
}
};
/**
* Compute edit distance between two strings.
*
* Uses Unicode characters (not UTF-16 code units),
* so emoji and supplementary plane characters are
* handled correctly.
*
* @example
* ```ts
* distance("Novak", "Nowak"); // 1
* distance("abcd", "abdc"); // 2
* distance("abcd", "abdc",
* "damerau-levenshtein"); // 1
* ```
*/
const distance = (a, b, metric) => binding.distance(a, b, metric ?? null);
//#endregion
//#region src/index.ts
initBinding(createRequire(import.meta.url)("../index.js"));
//#endregion
export { FuzzySearch, distance };
//# sourceMappingURL=index.mjs.map
{"version":3,"file":"index.mjs","names":[],"sources":["../src/core.ts","../src/index.ts"],"sourcesContent":["/* Shared core: types, helpers, and classes that\n * use a late-bound native backend (NAPI-RS or WASM).\n * Call initBinding() before constructing classes. */\n\n// -- Native binding types ----------------------------\n\nexport type NativeBinding = {\n FuzzySearch: new (\n entries: NormalizedEntry[],\n options?: Options,\n ) => NativeFuzzySearch;\n distance: (\n a: string,\n b: string,\n metric: Metric | null,\n ) => number;\n};\n\ntype NativeFuzzySearch = {\n patternCount: number;\n isMatch(haystack: string): boolean;\n _findIterPacked(haystack: string): Uint32Array;\n replaceAll(\n haystack: string,\n replacements: string[],\n ): string;\n};\n\ntype NormalizedEntry = {\n pattern: string;\n distance?: number;\n name?: string;\n};\n\n// -- Late-bound native binding -----------------------\n\nlet binding: NativeBinding;\n\n/** Set the native backend. Must be called once\n * before any class constructor. */\nexport const initBinding = (b: NativeBinding) => {\n binding = b;\n};\n\n// -- Public types ------------------------------------\n\n/** Distance metric for fuzzy matching. */\nexport type Metric = \"levenshtein\" | \"damerau-levenshtein\";\n\n/** Options for constructing a `FuzzySearch`. */\nexport type Options = {\n /**\n * Distance metric.\n * - `\"levenshtein\"`: insertions, deletions,\n * substitutions (default).\n * - `\"damerau-levenshtein\"`: + transpositions\n * of adjacent characters (ab -> ba = 1 edit).\n * @default \"levenshtein\"\n */\n metric?: Metric;\n /**\n * Strip diacritics before matching (NFD\n * decompose + remove combining marks).\n * \"Pribram\" matches \"Pribram\" at distance 0.\n * @default false\n */\n normalizeDiacritics?: boolean;\n /**\n * Use Unicode word boundaries (covers all\n * scripts). CJK characters are treated as\n * standalone words.\n * @default true\n */\n unicodeBoundaries?: boolean;\n /**\n * Only match whole words. Fuzzy matches on\n * substrings are usually noise; require word\n * boundaries unless opted out.\n * @default true\n */\n wholeWords?: boolean;\n /**\n * Case-insensitive matching (Unicode-aware).\n * @default false\n */\n caseInsensitive?: boolean;\n};\n\n/** A pattern entry with its edit distance. */\nexport type PatternEntry =\n | string\n | {\n pattern: string;\n /** Max edit distance. Must be less than\n * pattern length. `\"auto\"` uses the\n * Elasticsearch convention: 1-2 chars -> 0,\n * 3-5 chars -> 1, 6+ chars -> 2.\n * @default 1 */\n distance?: number | \"auto\";\n /** Optional name for the pattern. */\n name?: string;\n };\n\n/** A single fuzzy match result. */\nexport type FuzzyMatch = {\n /** Index into the patterns array. */\n pattern: number;\n /** Start UTF-16 code unit offset (compatible\n * with `String.prototype.slice()`). */\n start: number;\n /** End offset (exclusive). */\n end: number;\n /** The matched text\n * (`haystack.slice(start, end)`). */\n text: string;\n /** Actual Levenshtein edit distance. */\n distance: number;\n /** Pattern name (if provided). */\n name?: string;\n};\n\n// -- Internal helpers --------------------------------\n\nconst resolveDistance = (\n dist: number | \"auto\",\n patternLength: number,\n): number => {\n if (dist !== \"auto\") return dist;\n if (patternLength <= 2) return 0;\n if (patternLength <= 5) return 1;\n return 2;\n};\n\nconst normalizeEntry = (\n p: PatternEntry,\n i: number,\n): NormalizedEntry => {\n if (typeof p === \"string\") {\n return { pattern: p };\n }\n if (\n typeof p === \"object\" &&\n p !== null &&\n typeof p.pattern === \"string\"\n ) {\n if (p.distance === \"auto\") {\n return {\n ...p,\n distance: resolveDistance(\"auto\", p.pattern.length),\n };\n }\n // SAFETY: The \"auto\" case was already handled above,\n // so p.distance is number | undefined — matching\n // NormalizedEntry.\n return p as NormalizedEntry;\n }\n throw new TypeError(\n `Pattern at index ${i} must be a string ` +\n `or { pattern, distance?, name? }`,\n );\n};\n\nconst unpack = (\n packed: Uint32Array,\n haystack: string,\n names: (string | undefined)[],\n): FuzzyMatch[] => {\n const len = packed.length;\n const matches: FuzzyMatch[] = [];\n for (let i = 0; i < len; i += 4) {\n const idx = packed[i];\n const start = packed[i + 1];\n const end = packed[i + 2];\n const distance = packed[i + 3];\n if (\n idx === undefined ||\n start === undefined ||\n end === undefined ||\n distance === undefined\n ) {\n throw new Error(\n `Malformed packed array at offset ${String(i)}`,\n );\n }\n const m: FuzzyMatch = {\n pattern: idx,\n start,\n end,\n text: haystack.slice(start, end),\n distance,\n };\n if (names[idx] !== undefined) {\n m.name = names[idx];\n }\n matches.push(m);\n }\n return matches;\n};\n\n// -- Classes -----------------------------------------\n\n/**\n * Fuzzy string matcher. Finds approximate\n * matches within edit distance k, immune to\n * typos, OCR errors, and diacritics variants.\n *\n * Uses Myers' bit-parallel algorithm for O(n)\n * scanning per pattern (patterns up to 64 chars).\n *\n * @throws {Error} If a pattern is empty, too\n * long (> 64 chars), or distance > 3.\n *\n * @example\n * ```ts\n * const fs = new FuzzySearch([\n * { pattern: \"Gaislerova\", distance: 1 },\n * { pattern: \"Novak\", distance: 1 },\n * ], {\n * normalizeDiacritics: true,\n * wholeWords: true,\n * });\n *\n * fs.findIter(\"Gais1erova a Nowak\");\n * // [\n * // { pattern: 0, start: 0, end: 10,\n * // text: \"Gais1erova\", distance: 1 },\n * // { pattern: 1, start: 13, end: 18,\n * // text: \"Nowak\", distance: 1 },\n * // ]\n * ```\n */\nexport class FuzzySearch {\n private _names: (string | undefined)[];\n private _inner: NativeFuzzySearch;\n\n constructor(patterns: PatternEntry[], options?: Options) {\n const entries = patterns.map(normalizeEntry);\n this._names = entries.map((e) => e.name);\n this._inner = new binding.FuzzySearch(entries, options);\n }\n\n /** Number of patterns in the matcher. */\n get patternCount(): number {\n return this._inner.patternCount;\n }\n\n /**\n * Returns `true` if any pattern matches\n * within its edit distance.\n */\n isMatch(haystack: string): boolean {\n return this._inner.isMatch(haystack);\n }\n\n /** Find all non-overlapping fuzzy matches. */\n findIter(haystack: string): FuzzyMatch[] {\n return unpack(\n this._inner._findIterPacked(haystack),\n haystack,\n this._names,\n );\n }\n\n /**\n * Replace all fuzzy matches.\n * `replacements[i]` replaces pattern `i`.\n *\n * @throws {Error} If `replacements.length`\n * does not equal `patternCount`.\n */\n replaceAll(\n haystack: string,\n replacements: string[],\n ): string {\n return this._inner.replaceAll(haystack, replacements);\n }\n}\n\n/**\n * Compute edit distance between two strings.\n *\n * Uses Unicode characters (not UTF-16 code units),\n * so emoji and supplementary plane characters are\n * handled correctly.\n *\n * @example\n * ```ts\n * distance(\"Novak\", \"Nowak\"); // 1\n * distance(\"abcd\", \"abdc\"); // 2\n * distance(\"abcd\", \"abdc\",\n * \"damerau-levenshtein\"); // 1\n * ```\n */\nexport const distance = (\n a: string,\n b: string,\n metric?: Metric,\n): number => binding.distance(a, b, metric ?? null);\n","/* Main entry point — loads the native NAPI-RS\n * binding and re-exports the public API. */\n\nimport { createRequire } from \"node:module\";\n\nimport { initBinding, type NativeBinding } from \"./core\";\n\nconst require = createRequire(import.meta.url);\n// SAFETY: NAPI-RS auto-generated loader returns the\n// native binding object; its shape is validated by\n// usage in the core classes.\nconst native = require(\"../index.js\") as NativeBinding;\n\ninitBinding(native);\n\nexport { FuzzySearch, distance } from \"./core\";\n\nexport type {\n FuzzyMatch,\n Metric,\n NativeBinding,\n Options,\n PatternEntry,\n} from \"./core\";\n"],"mappings":";;AAoCA,IAAI;;;AAIJ,MAAa,eAAe,MAAqB;AAC/C,WAAU;;AAkFZ,MAAM,mBACJ,MACA,kBACW;AACX,KAAI,SAAS,OAAQ,QAAO;AAC5B,KAAI,iBAAiB,EAAG,QAAO;AAC/B,KAAI,iBAAiB,EAAG,QAAO;AAC/B,QAAO;;AAGT,MAAM,kBACJ,GACA,MACoB;AACpB,KAAI,OAAO,MAAM,SACf,QAAO,EAAE,SAAS,GAAG;AAEvB,KACE,OAAO,MAAM,YACb,MAAM,QACN,OAAO,EAAE,YAAY,UACrB;AACA,MAAI,EAAE,aAAa,OACjB,QAAO;GACL,GAAG;GACH,UAAU,gBAAgB,QAAQ,EAAE,QAAQ,OAAO;GACpD;AAKH,SAAO;;AAET,OAAM,IAAI,UACR,oBAAoB,EAAE,oDAEvB;;AAGH,MAAM,UACJ,QACA,UACA,UACiB;CACjB,MAAM,MAAM,OAAO;CACnB,MAAM,UAAwB,EAAE;AAChC,MAAK,IAAI,IAAI,GAAG,IAAI,KAAK,KAAK,GAAG;EAC/B,MAAM,MAAM,OAAO;EACnB,MAAM,QAAQ,OAAO,IAAI;EACzB,MAAM,MAAM,OAAO,IAAI;EACvB,MAAM,WAAW,OAAO,IAAI;AAC5B,MACE,QAAQ,KAAA,KACR,UAAU,KAAA,KACV,QAAQ,KAAA,KACR,aAAa,KAAA,EAEb,OAAM,IAAI,MACR,oCAAoC,OAAO,EAAE,GAC9C;EAEH,MAAM,IAAgB;GACpB,SAAS;GACT;GACA;GACA,MAAM,SAAS,MAAM,OAAO,IAAI;GAChC;GACD;AACD,MAAI,MAAM,SAAS,KAAA,EACjB,GAAE,OAAO,MAAM;AAEjB,UAAQ,KAAK,EAAE;;AAEjB,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCT,IAAa,cAAb,MAAyB;CACvB;CACA;CAEA,YAAY,UAA0B,SAAmB;EACvD,MAAM,UAAU,SAAS,IAAI,eAAe;AAC5C,OAAK,SAAS,QAAQ,KAAK,MAAM,EAAE,KAAK;AACxC,OAAK,SAAS,IAAI,QAAQ,YAAY,SAAS,QAAQ;;;CAIzD,IAAI,eAAuB;AACzB,SAAO,KAAK,OAAO;;;;;;CAOrB,QAAQ,UAA2B;AACjC,SAAO,KAAK,OAAO,QAAQ,SAAS;;;CAItC,SAAS,UAAgC;AACvC,SAAO,OACL,KAAK,OAAO,gBAAgB,SAAS,EACrC,UACA,KAAK,OACN;;;;;;;;;CAUH,WACE,UACA,cACQ;AACR,SAAO,KAAK,OAAO,WAAW,UAAU,aAAa;;;;;;;;;;;;;;;;;;AAmBzD,MAAa,YACX,GACA,GACA,WACW,QAAQ,SAAS,GAAG,GAAG,UAAU,KAAK;;;AC5RnD,YANgB,cAAc,OAAO,KAAK,IAAI,CAIvB,cAAc,CAElB"}
+7
-425

@@ -6,59 +6,5 @@ // prettier-ignore

const { readFileSync } = require('node:fs')
let nativeBinding = null
const loadErrors = []
const isMusl = () => {
let musl = false
if (process.platform === 'linux') {
musl = isMuslFromFilesystem()
if (musl === null) {
musl = isMuslFromReport()
}
if (musl === null) {
musl = isMuslFromChildProcess()
}
}
return musl
}
const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-')
const isMuslFromFilesystem = () => {
try {
return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl')
} catch {
return null
}
}
const isMuslFromReport = () => {
let report = null
if (typeof process.report?.getReport === 'function') {
process.report.excludeNetwork = true
report = process.report.getReport()
}
if (!report) {
return null
}
if (report.header && report.header.glibcVersionRuntime) {
return false
}
if (Array.isArray(report.sharedObjects)) {
if (report.sharedObjects.some(isFileMusl)) {
return true
}
}
return false
}
const isMuslFromChildProcess = () => {
try {
return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl')
} catch (e) {
// If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false
return false
}
}
function requireNative() {

@@ -71,108 +17,2 @@ if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {

}
} else if (process.platform === 'android') {
if (process.arch === 'arm64') {
try {
return require('./fuzzy-search.android-arm64.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-android-arm64')
const bindingPackageVersion = require('@stll/fuzzy-search-android-arm64/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm') {
try {
return require('./fuzzy-search.android-arm-eabi.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-android-arm-eabi')
const bindingPackageVersion = require('@stll/fuzzy-search-android-arm-eabi/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`))
}
} else if (process.platform === 'win32') {
if (process.arch === 'x64') {
if (process.config?.variables?.shlib_suffix === 'dll.a' || process.config?.variables?.node_target_type === 'shared_library') {
try {
return require('./fuzzy-search.win32-x64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-win32-x64-gnu')
const bindingPackageVersion = require('@stll/fuzzy-search-win32-x64-gnu/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./fuzzy-search.win32-x64-msvc.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-win32-x64-msvc')
const bindingPackageVersion = require('@stll/fuzzy-search-win32-x64-msvc/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'ia32') {
try {
return require('./fuzzy-search.win32-ia32-msvc.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-win32-ia32-msvc')
const bindingPackageVersion = require('@stll/fuzzy-search-win32-ia32-msvc/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm64') {
try {
return require('./fuzzy-search.win32-arm64-msvc.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-win32-arm64-msvc')
const bindingPackageVersion = require('@stll/fuzzy-search-win32-arm64-msvc/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`))
}
} else if (process.platform === 'darwin') {

@@ -229,6 +69,6 @@ try {

}
} else if (process.platform === 'freebsd') {
} else if (process.platform === 'linux') {
if (process.arch === 'x64') {
try {
return require('./fuzzy-search.freebsd-x64.node')
return require('./fuzzy-search.linux-x64-gnu.node')
} catch (e) {

@@ -238,4 +78,4 @@ loadErrors.push(e)

try {
const binding = require('@stll/fuzzy-search-freebsd-x64')
const bindingPackageVersion = require('@stll/fuzzy-search-freebsd-x64/package.json').version
const binding = require('@stll/fuzzy-search-linux-x64-gnu')
const bindingPackageVersion = require('@stll/fuzzy-search-linux-x64-gnu/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {

@@ -250,3 +90,3 @@ throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)

try {
return require('./fuzzy-search.freebsd-arm64.node')
return require('./fuzzy-search.linux-arm64-gnu.node')
} catch (e) {

@@ -256,4 +96,4 @@ loadErrors.push(e)

try {
const binding = require('@stll/fuzzy-search-freebsd-arm64')
const bindingPackageVersion = require('@stll/fuzzy-search-freebsd-arm64/package.json').version
const binding = require('@stll/fuzzy-search-linux-arm64-gnu')
const bindingPackageVersion = require('@stll/fuzzy-search-linux-arm64-gnu/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {

@@ -267,262 +107,4 @@ throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)

} else {
loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`))
}
} else if (process.platform === 'linux') {
if (process.arch === 'x64') {
if (isMusl()) {
try {
return require('./fuzzy-search.linux-x64-musl.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-linux-x64-musl')
const bindingPackageVersion = require('@stll/fuzzy-search-linux-x64-musl/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./fuzzy-search.linux-x64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-linux-x64-gnu')
const bindingPackageVersion = require('@stll/fuzzy-search-linux-x64-gnu/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'arm64') {
if (isMusl()) {
try {
return require('./fuzzy-search.linux-arm64-musl.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-linux-arm64-musl')
const bindingPackageVersion = require('@stll/fuzzy-search-linux-arm64-musl/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./fuzzy-search.linux-arm64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-linux-arm64-gnu')
const bindingPackageVersion = require('@stll/fuzzy-search-linux-arm64-gnu/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'arm') {
if (isMusl()) {
try {
return require('./fuzzy-search.linux-arm-musleabihf.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-linux-arm-musleabihf')
const bindingPackageVersion = require('@stll/fuzzy-search-linux-arm-musleabihf/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./fuzzy-search.linux-arm-gnueabihf.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-linux-arm-gnueabihf')
const bindingPackageVersion = require('@stll/fuzzy-search-linux-arm-gnueabihf/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'loong64') {
if (isMusl()) {
try {
return require('./fuzzy-search.linux-loong64-musl.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-linux-loong64-musl')
const bindingPackageVersion = require('@stll/fuzzy-search-linux-loong64-musl/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./fuzzy-search.linux-loong64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-linux-loong64-gnu')
const bindingPackageVersion = require('@stll/fuzzy-search-linux-loong64-gnu/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'riscv64') {
if (isMusl()) {
try {
return require('./fuzzy-search.linux-riscv64-musl.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-linux-riscv64-musl')
const bindingPackageVersion = require('@stll/fuzzy-search-linux-riscv64-musl/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
try {
return require('./fuzzy-search.linux-riscv64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-linux-riscv64-gnu')
const bindingPackageVersion = require('@stll/fuzzy-search-linux-riscv64-gnu/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
}
} else if (process.arch === 'ppc64') {
try {
return require('./fuzzy-search.linux-ppc64-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-linux-ppc64-gnu')
const bindingPackageVersion = require('@stll/fuzzy-search-linux-ppc64-gnu/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 's390x') {
try {
return require('./fuzzy-search.linux-s390x-gnu.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-linux-s390x-gnu')
const bindingPackageVersion = require('@stll/fuzzy-search-linux-s390x-gnu/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`))
}
} else if (process.platform === 'openharmony') {
if (process.arch === 'arm64') {
try {
return require('./fuzzy-search.openharmony-arm64.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-openharmony-arm64')
const bindingPackageVersion = require('@stll/fuzzy-search-openharmony-arm64/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'x64') {
try {
return require('./fuzzy-search.openharmony-x64.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-openharmony-x64')
const bindingPackageVersion = require('@stll/fuzzy-search-openharmony-x64/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else if (process.arch === 'arm') {
try {
return require('./fuzzy-search.openharmony-arm.node')
} catch (e) {
loadErrors.push(e)
}
try {
const binding = require('@stll/fuzzy-search-openharmony-arm')
const bindingPackageVersion = require('@stll/fuzzy-search-openharmony-arm/package.json').version
if (bindingPackageVersion !== '0.0.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') {
throw new Error(`Native binding package version mismatch, expected 0.0.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`)
}
return binding
} catch (e) {
loadErrors.push(e)
}
} else {
loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`))
}
} else {

@@ -529,0 +111,0 @@ loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`))

{
"name": "@stll/fuzzy-search",
"version": "0.0.2",
"publishConfig": {
"access": "restricted"
},
"version": "0.1.1",
"description": "NAPI-RS bindings for fuzzy string matching via Myers' bit-parallel algorithm for Node.js/Bun",

@@ -31,14 +28,13 @@ "keywords": [

"type": "module",
"types": "dist/index.d.ts",
"types": "dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"browser": {
"types": "./dist/wasm.d.ts",
"import": "./dist/wasm.js"
},
"import": "./dist/index.js",
"default": "./dist/index.js"
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"default": "./dist/index.mjs"
}
},
"publishConfig": {
"access": "restricted"
},
"scripts": {

@@ -64,19 +60,19 @@ "build": "napi build --platform --release",

"devDependencies": {
"@emnapi/core": "^1.9.0",
"@emnapi/runtime": "^1.9.0",
"@napi-rs/cli": "^3.0.0",
"@emnapi/core": "^1.9.1",
"@emnapi/runtime": "^1.9.1",
"@napi-rs/cli": "^3.5.1",
"@tybys/wasm-util": "^0.10.1",
"@types/node": "^25.5.0",
"bun-types": "^1.3.10",
"emnapi": "^1.9.0",
"bun-types": "^1.3.11",
"emnapi": "^1.9.1",
"fast-check": "^4.6.0",
"oxfmt": "^0.40.0",
"oxlint": "^1.55.0",
"tsdown": "^0.12.4"
"oxfmt": "^0.42.0",
"oxlint": "^1.57.0",
"tsdown": "^0.21.6"
},
"optionalDependencies": {
"@stll/fuzzy-search-darwin-arm64": "0.0.2",
"@stll/fuzzy-search-darwin-x64": "0.0.2",
"@stll/fuzzy-search-darwin-arm64": "0.0.2",
"@stll/fuzzy-search-linux-arm64-gnu": "0.0.2",
"@stll/fuzzy-search-linux-x64-gnu": "0.0.2",
"@stll/fuzzy-search-linux-arm64-gnu": "0.0.2",
"@stll/fuzzy-search-wasm32-wasi": "0.0.2"

@@ -97,2 +93,2 @@ },

}
}
}
//#region src/core.d.ts
type NativeBinding = {
FuzzySearch: new (entries: NormalizedEntry[], options?: Options) => NativeFuzzySearch;
distance: (a: string, b: string, metric: Metric | null) => number;
};
type NativeFuzzySearch = {
patternCount: number;
isMatch(haystack: string): boolean;
_findIterPacked(haystack: string): Uint32Array;
replaceAll(haystack: string, replacements: string[]): string;
};
type NormalizedEntry = {
pattern: string;
distance?: number;
name?: string;
};
/** Set the native backend. Must be called once
* before any class constructor. */
/** Distance metric for fuzzy matching. */
type Metric = "levenshtein" | "damerau-levenshtein";
/** Options for constructing a `FuzzySearch`. */
type Options = {
/**
* Distance metric.
* - `"levenshtein"`: insertions, deletions,
* substitutions (default).
* - `"damerau-levenshtein"`: + transpositions
* of adjacent characters (ab -> ba = 1 edit).
* @default "levenshtein"
*/
metric?: Metric;
/**
* Strip diacritics before matching (NFD
* decompose + remove combining marks).
* "Pribram" matches "Pribram" at distance 0.
* @default false
*/
normalizeDiacritics?: boolean;
/**
* Use Unicode word boundaries (covers all
* scripts). CJK characters are treated as
* standalone words.
* @default true
*/
unicodeBoundaries?: boolean;
/**
* Only match whole words. Fuzzy matches on
* substrings are usually noise; require word
* boundaries unless opted out.
* @default true
*/
wholeWords?: boolean;
/**
* Case-insensitive matching (Unicode-aware).
* @default false
*/
caseInsensitive?: boolean;
};
/** A pattern entry with its edit distance. */
type PatternEntry = string | {
pattern: string;
/** Max edit distance. Must be less than
* pattern length. `"auto"` uses the
* Elasticsearch convention: 1-2 chars -> 0,
* 3-5 chars -> 1, 6+ chars -> 2.
* @default 1 */
distance?: number | "auto";
/** Optional name for the pattern. */
name?: string;
};
/** A single fuzzy match result. */
type FuzzyMatch = {
/** Index into the patterns array. */
pattern: number;
/** Start UTF-16 code unit offset (compatible
* with `String.prototype.slice()`). */
start: number;
/** End offset (exclusive). */
end: number;
/** The matched text
* (`haystack.slice(start, end)`). */
text: string;
/** Actual Levenshtein edit distance. */
distance: number;
/** Pattern name (if provided). */
name?: string;
};
/**
* Fuzzy string matcher. Finds approximate
* matches within edit distance k, immune to
* typos, OCR errors, and diacritics variants.
*
* Uses Myers' bit-parallel algorithm for O(n)
* scanning per pattern (patterns up to 64 chars).
*
* @throws {Error} If a pattern is empty, too
* long (> 64 chars), or distance > 3.
*
* @example
* ```ts
* const fs = new FuzzySearch([
* { pattern: "Gaislerova", distance: 1 },
* { pattern: "Novak", distance: 1 },
* ], {
* normalizeDiacritics: true,
* wholeWords: true,
* });
*
* fs.findIter("Gais1erova a Nowak");
* // [
* // { pattern: 0, start: 0, end: 10,
* // text: "Gais1erova", distance: 1 },
* // { pattern: 1, start: 13, end: 18,
* // text: "Nowak", distance: 1 },
* // ]
* ```
*/
declare class FuzzySearch {
private _names;
private _inner;
constructor(patterns: PatternEntry[], options?: Options);
/** Number of patterns in the matcher. */
get patternCount(): number;
/**
* Returns `true` if any pattern matches
* within its edit distance.
*/
isMatch(haystack: string): boolean;
/** Find all non-overlapping fuzzy matches. */
findIter(haystack: string): FuzzyMatch[];
/**
* Replace all fuzzy matches.
* `replacements[i]` replaces pattern `i`.
*
* @throws {Error} If `replacements.length`
* does not equal `patternCount`.
*/
replaceAll(haystack: string, replacements: string[]): string;
}
/**
* Compute edit distance between two strings.
*
* Uses Unicode characters (not UTF-16 code units),
* so emoji and supplementary plane characters are
* handled correctly.
*
* @example
* ```ts
* distance("Novak", "Nowak"); // 1
* distance("abcd", "abdc"); // 2
* distance("abcd", "abdc",
* "damerau-levenshtein"); // 1
* ```
*/
declare const distance: (a: string, b: string, metric?: Metric) => number;
//#endregion
export { Options as a, NativeBinding as i, FuzzySearch as n, PatternEntry as o, Metric as r, distance as s, FuzzyMatch as t };
//# sourceMappingURL=core.d.ts.map
//#region src/core.ts
let binding;
/** Set the native backend. Must be called once
* before any class constructor. */
const initBinding = (b) => {
binding = b;
};
const resolveDistance = (dist, patternLength) => {
if (dist !== "auto") return dist;
if (patternLength <= 2) return 0;
if (patternLength <= 5) return 1;
return 2;
};
const normalizeEntry = (p, i) => {
if (typeof p === "string") return { pattern: p };
if (typeof p === "object" && p !== null && typeof p.pattern === "string") {
if (p.distance === "auto") return {
...p,
distance: resolveDistance("auto", p.pattern.length)
};
return p;
}
throw new TypeError(`Pattern at index ${i} must be a string or { pattern, distance?, name? }`);
};
const unpack = (packed, haystack, names) => {
const len = packed.length;
const matches = new Array(len / 4);
for (let i = 0, j = 0; i < len; i += 4, j++) {
const idx = packed[i];
const start = packed[i + 1];
const end = packed[i + 2];
const m = {
pattern: idx,
start,
end,
text: haystack.slice(start, end),
distance: packed[i + 3]
};
if (names[idx] !== void 0) m.name = names[idx];
matches[j] = m;
}
return matches;
};
/**
* Fuzzy string matcher. Finds approximate
* matches within edit distance k, immune to
* typos, OCR errors, and diacritics variants.
*
* Uses Myers' bit-parallel algorithm for O(n)
* scanning per pattern (patterns up to 64 chars).
*
* @throws {Error} If a pattern is empty, too
* long (> 64 chars), or distance > 3.
*
* @example
* ```ts
* const fs = new FuzzySearch([
* { pattern: "Gaislerova", distance: 1 },
* { pattern: "Novak", distance: 1 },
* ], {
* normalizeDiacritics: true,
* wholeWords: true,
* });
*
* fs.findIter("Gais1erova a Nowak");
* // [
* // { pattern: 0, start: 0, end: 10,
* // text: "Gais1erova", distance: 1 },
* // { pattern: 1, start: 13, end: 18,
* // text: "Nowak", distance: 1 },
* // ]
* ```
*/
var FuzzySearch = class {
_names;
_inner;
constructor(patterns, options) {
const entries = patterns.map(normalizeEntry);
this._names = entries.map((e) => e.name);
this._inner = new binding.FuzzySearch(entries, options);
}
/** Number of patterns in the matcher. */
get patternCount() {
return this._inner.patternCount;
}
/**
* Returns `true` if any pattern matches
* within its edit distance.
*/
isMatch(haystack) {
return this._inner.isMatch(haystack);
}
/** Find all non-overlapping fuzzy matches. */
findIter(haystack) {
return unpack(this._inner._findIterPacked(haystack), haystack, this._names);
}
/**
* Replace all fuzzy matches.
* `replacements[i]` replaces pattern `i`.
*
* @throws {Error} If `replacements.length`
* does not equal `patternCount`.
*/
replaceAll(haystack, replacements) {
return this._inner.replaceAll(haystack, replacements);
}
};
/**
* Compute edit distance between two strings.
*
* Uses Unicode characters (not UTF-16 code units),
* so emoji and supplementary plane characters are
* handled correctly.
*
* @example
* ```ts
* distance("Novak", "Nowak"); // 1
* distance("abcd", "abdc"); // 2
* distance("abcd", "abdc",
* "damerau-levenshtein"); // 1
* ```
*/
const distance = (a, b, metric) => binding.distance(a, b, metric ?? null);
//#endregion
export { distance as n, initBinding as r, FuzzySearch as t };
//# sourceMappingURL=core.js.map
{"version":3,"file":"core.js","names":[],"sources":["../src/core.ts"],"sourcesContent":["/* Shared core: types, helpers, and classes that\n * use a late-bound native backend (NAPI-RS or WASM).\n * Call initBinding() before constructing classes. */\n\n// -- Native binding types ----------------------------\n\nexport type NativeBinding = {\n FuzzySearch: new (\n entries: NormalizedEntry[],\n options?: Options,\n ) => NativeFuzzySearch;\n distance: (\n a: string,\n b: string,\n metric: Metric | null,\n ) => number;\n};\n\ntype NativeFuzzySearch = {\n patternCount: number;\n isMatch(haystack: string): boolean;\n _findIterPacked(haystack: string): Uint32Array;\n replaceAll(\n haystack: string,\n replacements: string[],\n ): string;\n};\n\ntype NormalizedEntry = {\n pattern: string;\n distance?: number;\n name?: string;\n};\n\n// -- Late-bound native binding -----------------------\n\nlet binding: NativeBinding;\n\n/** Set the native backend. Must be called once\n * before any class constructor. */\nexport const initBinding = (b: NativeBinding) => {\n binding = b;\n};\n\n// -- Public types ------------------------------------\n\n/** Distance metric for fuzzy matching. */\nexport type Metric = \"levenshtein\" | \"damerau-levenshtein\";\n\n/** Options for constructing a `FuzzySearch`. */\nexport type Options = {\n /**\n * Distance metric.\n * - `\"levenshtein\"`: insertions, deletions,\n * substitutions (default).\n * - `\"damerau-levenshtein\"`: + transpositions\n * of adjacent characters (ab -> ba = 1 edit).\n * @default \"levenshtein\"\n */\n metric?: Metric;\n /**\n * Strip diacritics before matching (NFD\n * decompose + remove combining marks).\n * \"Pribram\" matches \"Pribram\" at distance 0.\n * @default false\n */\n normalizeDiacritics?: boolean;\n /**\n * Use Unicode word boundaries (covers all\n * scripts). CJK characters are treated as\n * standalone words.\n * @default true\n */\n unicodeBoundaries?: boolean;\n /**\n * Only match whole words. Fuzzy matches on\n * substrings are usually noise; require word\n * boundaries unless opted out.\n * @default true\n */\n wholeWords?: boolean;\n /**\n * Case-insensitive matching (Unicode-aware).\n * @default false\n */\n caseInsensitive?: boolean;\n};\n\n/** A pattern entry with its edit distance. */\nexport type PatternEntry =\n | string\n | {\n pattern: string;\n /** Max edit distance. Must be less than\n * pattern length. `\"auto\"` uses the\n * Elasticsearch convention: 1-2 chars -> 0,\n * 3-5 chars -> 1, 6+ chars -> 2.\n * @default 1 */\n distance?: number | \"auto\";\n /** Optional name for the pattern. */\n name?: string;\n };\n\n/** A single fuzzy match result. */\nexport type FuzzyMatch = {\n /** Index into the patterns array. */\n pattern: number;\n /** Start UTF-16 code unit offset (compatible\n * with `String.prototype.slice()`). */\n start: number;\n /** End offset (exclusive). */\n end: number;\n /** The matched text\n * (`haystack.slice(start, end)`). */\n text: string;\n /** Actual Levenshtein edit distance. */\n distance: number;\n /** Pattern name (if provided). */\n name?: string;\n};\n\n// -- Internal helpers --------------------------------\n\nconst resolveDistance = (\n dist: number | \"auto\",\n patternLength: number,\n): number => {\n if (dist !== \"auto\") return dist;\n if (patternLength <= 2) return 0;\n if (patternLength <= 5) return 1;\n return 2;\n};\n\nconst normalizeEntry = (\n p: PatternEntry,\n i: number,\n): NormalizedEntry => {\n if (typeof p === \"string\") {\n return { pattern: p };\n }\n if (\n typeof p === \"object\" &&\n p !== null &&\n typeof p.pattern === \"string\"\n ) {\n if (p.distance === \"auto\") {\n return {\n ...p,\n distance: resolveDistance(\"auto\", p.pattern.length),\n };\n }\n // SAFETY: The \"auto\" case was already handled above,\n // so p.distance is number | undefined — matching\n // NormalizedEntry.\n return p as NormalizedEntry;\n }\n throw new TypeError(\n `Pattern at index ${i} must be a string ` +\n `or { pattern, distance?, name? }`,\n );\n};\n\nconst unpack = (\n packed: Uint32Array,\n haystack: string,\n names: (string | undefined)[],\n): FuzzyMatch[] => {\n // SAFETY: Loop increments by 4 and terminates at packed.length.\n // Indices i through i+3 are always in bounds.\n const len = packed.length;\n // eslint-disable-next-line unicorn/no-new-array\n const matches = new Array<FuzzyMatch>(len / 4);\n for (let i = 0, j = 0; i < len; i += 4, j++) {\n const idx = packed[i]!;\n const start = packed[i + 1]!;\n const end = packed[i + 2]!;\n const m: FuzzyMatch = {\n pattern: idx,\n start,\n end,\n text: haystack.slice(start, end),\n distance: packed[i + 3]!,\n };\n if (names[idx] !== undefined) {\n m.name = names[idx];\n }\n matches[j] = m;\n }\n return matches;\n};\n\n// -- Classes -----------------------------------------\n\n/**\n * Fuzzy string matcher. Finds approximate\n * matches within edit distance k, immune to\n * typos, OCR errors, and diacritics variants.\n *\n * Uses Myers' bit-parallel algorithm for O(n)\n * scanning per pattern (patterns up to 64 chars).\n *\n * @throws {Error} If a pattern is empty, too\n * long (> 64 chars), or distance > 3.\n *\n * @example\n * ```ts\n * const fs = new FuzzySearch([\n * { pattern: \"Gaislerova\", distance: 1 },\n * { pattern: \"Novak\", distance: 1 },\n * ], {\n * normalizeDiacritics: true,\n * wholeWords: true,\n * });\n *\n * fs.findIter(\"Gais1erova a Nowak\");\n * // [\n * // { pattern: 0, start: 0, end: 10,\n * // text: \"Gais1erova\", distance: 1 },\n * // { pattern: 1, start: 13, end: 18,\n * // text: \"Nowak\", distance: 1 },\n * // ]\n * ```\n */\nexport class FuzzySearch {\n private _names: (string | undefined)[];\n private _inner: NativeFuzzySearch;\n\n constructor(patterns: PatternEntry[], options?: Options) {\n const entries = patterns.map(normalizeEntry);\n this._names = entries.map((e) => e.name);\n this._inner = new binding.FuzzySearch(entries, options);\n }\n\n /** Number of patterns in the matcher. */\n get patternCount(): number {\n return this._inner.patternCount;\n }\n\n /**\n * Returns `true` if any pattern matches\n * within its edit distance.\n */\n isMatch(haystack: string): boolean {\n return this._inner.isMatch(haystack);\n }\n\n /** Find all non-overlapping fuzzy matches. */\n findIter(haystack: string): FuzzyMatch[] {\n return unpack(\n this._inner._findIterPacked(haystack),\n haystack,\n this._names,\n );\n }\n\n /**\n * Replace all fuzzy matches.\n * `replacements[i]` replaces pattern `i`.\n *\n * @throws {Error} If `replacements.length`\n * does not equal `patternCount`.\n */\n replaceAll(\n haystack: string,\n replacements: string[],\n ): string {\n return this._inner.replaceAll(haystack, replacements);\n }\n}\n\n/**\n * Compute edit distance between two strings.\n *\n * Uses Unicode characters (not UTF-16 code units),\n * so emoji and supplementary plane characters are\n * handled correctly.\n *\n * @example\n * ```ts\n * distance(\"Novak\", \"Nowak\"); // 1\n * distance(\"abcd\", \"abdc\"); // 2\n * distance(\"abcd\", \"abdc\",\n * \"damerau-levenshtein\"); // 1\n * ```\n */\nexport const distance = (\n a: string,\n b: string,\n metric?: Metric,\n): number => binding.distance(a, b, metric ?? null);\n"],"mappings":";AAoCA,IAAI;;;AAIJ,MAAa,eAAe,MAAqB;AAC/C,WAAU;;AAkFZ,MAAM,mBACJ,MACA,kBACW;AACX,KAAI,SAAS,OAAQ,QAAO;AAC5B,KAAI,iBAAiB,EAAG,QAAO;AAC/B,KAAI,iBAAiB,EAAG,QAAO;AAC/B,QAAO;;AAGT,MAAM,kBACJ,GACA,MACoB;AACpB,KAAI,OAAO,MAAM,SACf,QAAO,EAAE,SAAS,GAAG;AAEvB,KACE,OAAO,MAAM,YACb,MAAM,QACN,OAAO,EAAE,YAAY,UACrB;AACA,MAAI,EAAE,aAAa,OACjB,QAAO;GACL,GAAG;GACH,UAAU,gBAAgB,QAAQ,EAAE,QAAQ,OAAO;GACpD;AAKH,SAAO;;AAET,OAAM,IAAI,UACR,oBAAoB,EAAE,oDAEvB;;AAGH,MAAM,UACJ,QACA,UACA,UACiB;CAGjB,MAAM,MAAM,OAAO;CAEnB,MAAM,UAAU,IAAI,MAAkB,MAAM,EAAE;AAC9C,MAAK,IAAI,IAAI,GAAG,IAAI,GAAG,IAAI,KAAK,KAAK,GAAG,KAAK;EAC3C,MAAM,MAAM,OAAO;EACnB,MAAM,QAAQ,OAAO,IAAI;EACzB,MAAM,MAAM,OAAO,IAAI;EACvB,MAAM,IAAgB;GACpB,SAAS;GACT;GACA;GACA,MAAM,SAAS,MAAM,OAAO,IAAI;GAChC,UAAU,OAAO,IAAI;GACtB;AACD,MAAI,MAAM,SAAS,KAAA,EACjB,GAAE,OAAO,MAAM;AAEjB,UAAQ,KAAK;;AAEf,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCT,IAAa,cAAb,MAAyB;CACvB;CACA;CAEA,YAAY,UAA0B,SAAmB;EACvD,MAAM,UAAU,SAAS,IAAI,eAAe;AAC5C,OAAK,SAAS,QAAQ,KAAK,MAAM,EAAE,KAAK;AACxC,OAAK,SAAS,IAAI,QAAQ,YAAY,SAAS,QAAQ;;;CAIzD,IAAI,eAAuB;AACzB,SAAO,KAAK,OAAO;;;;;;CAOrB,QAAQ,UAA2B;AACjC,SAAO,KAAK,OAAO,QAAQ,SAAS;;;CAItC,SAAS,UAAgC;AACvC,SAAO,OACL,KAAK,OAAO,gBAAgB,SAAS,EACrC,UACA,KAAK,OACN;;;;;;;;;CAUH,WACE,UACA,cACQ;AACR,SAAO,KAAK,OAAO,WAAW,UAAU,aAAa;;;;;;;;;;;;;;;;;;AAmBzD,MAAa,YACX,GACA,GACA,WACW,QAAQ,SAAS,GAAG,GAAG,UAAU,KAAK"}
import { a as Options, i as NativeBinding, n as FuzzySearch, o as PatternEntry, r as Metric, s as distance, t as FuzzyMatch } from "./core.js";
export { type FuzzyMatch, FuzzySearch, type Metric, type NativeBinding, type Options, type PatternEntry, distance };
import { n as distance, r as initBinding, t as FuzzySearch } from "./core.js";
import { createRequire } from "node:module";
//#region src/index.ts
initBinding(createRequire(import.meta.url)("../index.js"));
//#endregion
export { FuzzySearch, distance };
//# sourceMappingURL=index.js.map
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["/* Main entry point — loads the native NAPI-RS\n * binding and re-exports the public API. */\n\nimport { createRequire } from \"node:module\";\n\nimport { initBinding, type NativeBinding } from \"./core\";\n\nconst require = createRequire(import.meta.url);\n// SAFETY: NAPI-RS auto-generated loader returns the\n// native binding object; its shape is validated by\n// usage in the core classes.\nconst native = require(\"../index.js\") as NativeBinding;\n\ninitBinding(native);\n\nexport { FuzzySearch, distance } from \"./core\";\n\nexport type {\n FuzzyMatch,\n Metric,\n NativeBinding,\n Options,\n PatternEntry,\n} from \"./core\";\n"],"mappings":";;;AAaA,YANgB,cAAc,OAAO,KAAK,IAAI,CAIvB,cAAc,CAElB"}
import { a as Options, i as NativeBinding, n as FuzzySearch, o as PatternEntry, r as Metric, s as distance, t as FuzzyMatch } from "./core.js";
export { type FuzzyMatch, FuzzySearch, type Metric, type NativeBinding, type Options, type PatternEntry, distance };
import { n as distance, r as initBinding, t as FuzzySearch } from "./core.js";
import native from "@stll/fuzzy-search-wasm32-wasi";
//#region src/wasm.ts
initBinding(native);
//#endregion
export { FuzzySearch, distance };
//# sourceMappingURL=wasm.js.map
{"version":3,"file":"wasm.js","names":[],"sources":["../src/wasm.ts"],"sourcesContent":["/* Browser/WASM entry point — loads the binding from\n * the wasm32-wasi sub-package and re-exports the\n * public API through the shared core. */\n\nimport native from \"@stll/fuzzy-search-wasm32-wasi\";\nimport { initBinding, type NativeBinding } from \"./core\";\n\ninitBinding(native as unknown as NativeBinding);\n\nexport { FuzzySearch, distance } from \"./core\";\n\nexport type {\n FuzzyMatch,\n Metric,\n NativeBinding,\n Options,\n PatternEntry,\n} from \"./core\";\n"],"mappings":";;;AAOA,YAAY,OAAmC"}