/** Normalize a display name for comparison: lowercase, strip punctuation, collapse whitespace. */ export function normalizeName(name: string): string { return name.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, ' ').trim(); } function bigrams(s: string): string[] { const result: string[] = []; for (let i = 0; i < s.length - 1; i++) result.push(s.slice(i, i + 2)); return result; } /** Bigram Dice coefficient — returns similarity in [0, 1]. */ export function diceCoefficient(a: string, b: string): number { if (a === b) return 1; if (a.length < 2 || b.length < 2) return 0; const bigA = bigrams(a); const bigB = new Set(bigrams(b)); const intersection = bigA.filter((bg) => bigB.has(bg)).length; return (2 * intersection) / (bigA.length + bigB.size); }