/** Levenshtein edit distance between two strings */ export function editDistance(a: string, b: string): number { const m = a.length, n = b.length; const dp: number[] = Array(n + 1).fill(0).map((_, j) => j); for (let i = 1; i <= m; i++) { let prev = dp[0]; dp[0] = i; for (let j = 1; j <= n; j++) { const temp = dp[j]; dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]); prev = temp; } } return dp[n]; } /** Return a score 0–1 for how well a user matches a search query */ export function scoreUser(u: any, raw: string): number { const q = raw.toLowerCase().trim(); if (!q || q.length < 2) return 0; const name = (u.name || "").toLowerCase(); const email = (u.email || "").toLowerCase(); const phone = (u.phoneNumber || "").replace(/\D/g, ""); const qPhone = q.replace(/\D/g, ""); let best = 0; // Exact substring matches — highest priority if (name.includes(q)) best = Math.max(best, 1.0); if (email.includes(q)) best = Math.max(best, 0.95); if (qPhone.length >= 3 && phone.includes(qPhone)) best = Math.max(best, 0.95); // Word-level starts-with (handles partial first/last name) const words = name.split(/\s+/); if (words.some((w: string) => w.startsWith(q))) best = Math.max(best, 0.85); // Fuzzy edit-distance against each name word const maxDist = q.length <= 4 ? 1 : q.length <= 7 ? 2 : 3; for (const w of words) { const slice = w.slice(0, q.length + 2); // compare against similar-length slice const d = editDistance(q, slice); if (d <= maxDist) best = Math.max(best, 0.75 - d * 0.15); } // Also fuzzy against the full name (catches "jhn smth" → "john smith") const nameSlice = name.slice(0, q.length + 4); const fullDist = editDistance(q, nameSlice); if (fullDist <= maxDist + 1) best = Math.max(best, 0.6 - fullDist * 0.12); return best; }