export interface SearchResult { items: T[]; fuzzy: boolean; } export function normalizeSearch(value: string): string { return value.normalize("NFC").trim().toLocaleLowerCase(); } // Bounded optimal-string-alignment distance, including adjacent transpositions. function editDistance(left: string[], right: string[], limit: number): number { if (Math.abs(left.length - right.length) > limit) return limit + 1; let previous = Array.from({ length: right.length + 1 }, (_, index) => index); let beforePrevious = previous; for (let i = 1; i <= left.length; i++) { const current = Array(right.length + 1).fill(limit + 1); current[0] = i; for (let j = Math.max(1, i - limit); j <= Math.min(right.length, i + limit); j++) { current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + (left[i - 1] === right[j - 1] ? 0 : 1)); if (i > 1 && j > 1 && left[i - 1] === right[j - 2] && left[i - 2] === right[j - 1]) { current[j] = Math.min(current[j], beforePrevious[j - 2] + 1); } } beforePrevious = previous; previous = current; } return previous[right.length]; } export function searchItems( items: readonly T[], query: string, directText: (item: T) => string, names: (item: T) => readonly string[], ): SearchResult { const normalized = normalizeSearch(query); if (!normalized) return { items: [...items], fuzzy: false }; const direct = items.filter((item) => normalizeSearch(directText(item)).includes(normalized)); if (direct.length) return { items: direct, fuzzy: false }; const characters = Array.from(normalized); if (characters.length < 4) return { items: [], fuzzy: false }; const limit = Math.min(characters.length < 8 ? 1 : 2, Math.floor(characters.length / 4)); const matches = items.map((item, index) => { let distance = limit + 1; for (const name of names(item)) { const candidate = normalizeSearch(name); for (const part of new Set([candidate, ...candidate.split(/\s+/u)])) { distance = Math.min(distance, editDistance(characters, Array.from(part), limit)); } } return { item, index, distance }; }).filter(({ distance }) => distance <= limit) .sort((a, b) => a.distance - b.distance || a.index - b.index); return { items: matches.map(({ item }) => item), fuzzy: matches.length > 0 }; }