import type { FullContact } from "./types" import { fullContactDisplayName } from "./types" function contactMergeScore(c: FullContact): number { let score = 0 if (fullContactDisplayName(c)) score += 3 if (c.emails.some((e) => e.value.trim())) score += 4 if (c.phones.some((p) => p.value.trim())) score += 2 if (c.company?.trim()) score += 1 if (c.path?.trim()) score += 1 return score } export function pickContactMergePrimary(contacts: FullContact[]): FullContact | undefined { if (contacts.length === 0) return undefined return [...contacts].sort((a, b) => contactMergeScore(b) - contactMergeScore(a))[0] } export function mergeManyContacts( contacts: FullContact[], primaryId?: string, ): { primary: FullContact; secondaries: FullContact[]; merged: FullContact } | null { if (contacts.length < 2) return null const primary = (primaryId ? contacts.find((c) => c.id === primaryId) : undefined) ?? pickContactMergePrimary(contacts) if (!primary) return null const secondaries = contacts.filter((c) => c.id !== primary.id) let merged = primary for (const secondary of secondaries) { merged = mergeFullContactFields(merged, secondary) } return { primary, secondaries, merged: { ...merged, id: primary.id, path: primary.path, etag: primary.etag, createdAt: primary.createdAt, }, } } function dedupeLabeledValues(items: T[]): T[] { const seen = new Set() const out: T[] = [] for (const item of items) { const key = item.value.trim().toLowerCase() if (!key || seen.has(key)) continue seen.add(key) out.push(item) } return out } function dedupeAddresses(items: NonNullable): NonNullable { const seen = new Set() const out: NonNullable = [] for (const item of items) { const key = [ item.street, item.city, item.region, item.postalCode, item.country, item.label, ] .map((part) => part?.trim().toLowerCase() ?? "") .join("|") if (!key.replace(/\|/g, "") || seen.has(key)) continue seen.add(key) out.push(item) } return out } function dedupeStrings(items: string[]): string[] { const seen = new Set() const out: string[] = [] for (const item of items) { const key = item.trim().toLowerCase() if (!key || seen.has(key)) continue seen.add(key) out.push(item) } return out } function mergeNotes(a?: string, b?: string): string | undefined { const parts = [a, b] .map((n) => n?.trim()) .filter(Boolean) as string[] if (parts.length === 0) return undefined return dedupeStrings(parts).join("\n\n") } export function mergeFullContactFields(primary: FullContact, secondary: FullContact): FullContact { return { ...primary, namePrefix: primary.namePrefix || secondary.namePrefix, firstName: primary.firstName || secondary.firstName, middleName: primary.middleName || secondary.middleName, lastName: primary.lastName || secondary.lastName, nameSuffix: primary.nameSuffix || secondary.nameSuffix, phoneticFirstName: primary.phoneticFirstName || secondary.phoneticFirstName, phoneticLastName: primary.phoneticLastName || secondary.phoneticLastName, company: primary.company || secondary.company, department: primary.department || secondary.department, jobTitle: primary.jobTitle || secondary.jobTitle, website: primary.website || secondary.website, notes: mergeNotes(primary.notes, secondary.notes), emails: dedupeLabeledValues([...primary.emails, ...secondary.emails]), phones: dedupeLabeledValues([...primary.phones, ...secondary.phones]), addresses: dedupeAddresses([ ...(primary.addresses ?? []), ...(secondary.addresses ?? []), ]), nicknames: dedupeStrings([...(primary.nicknames ?? []), ...(secondary.nicknames ?? [])]), labels: dedupeStrings([...(primary.labels ?? []), ...(secondary.labels ?? [])]), avatarUrl: primary.avatarUrl || secondary.avatarUrl, birthday: primary.birthday ?? secondary.birthday, updatedAt: Date.now(), } } export function mergeTwoContacts( a: FullContact, b: FullContact, ): { primary: FullContact; secondary: FullContact; merged: FullContact } { const [primary, secondary] = contactMergeScore(a) >= contactMergeScore(b) ? [a, b] : [b, a] return { primary, secondary, merged: mergeFullContactFields(primary, secondary) } }