ContactDraftMerge.swift 39 lignes · 1792 octets
import Foundation

/// Merges sources, by reliability priority:
/// 1. OCR regexes (phone / email / url) — always win
/// 2. Optional LLM card (`llm`) if present and non-empty — Card2vcf passes `nil`
/// 3. Deterministic heuristics (name / company / job / address)
enum ContactDraftMerge {
    static func merge(llm: ContactCard?, ocr: OcrResult) -> ContactCard {
        let heuristic = ContactHeuristicParser.parse(ocr)
        let base = llm ?? ContactCard()
        return ContactCard(
            fullName: prefer(base.fullName, heuristic.fullName),
            firstName: prefer(base.firstName, heuristic.firstName),
            lastName: prefer(base.lastName, heuristic.lastName),
            company: prefer(base.company, heuristic.company),
            jobTitle: prefer(base.jobTitle, heuristic.jobTitle),
            phones: mergeStrings(preferred: ocr.phones, extra: base.phones + heuristic.phones),
            emails: mergeStrings(preferred: ocr.emails, extra: base.emails + heuristic.emails),
            website: prefer(ocr.urls.first, prefer(base.website, heuristic.website)),
            address: prefer(base.address, heuristic.address),
            note: prefer(base.note, heuristic.note),
            champsDouteux: heuristic.champsDouteux
        )
    }

    private static func prefer(_ primary: String?, _ fallback: String?) -> String? {
        primary?.nilIfBlank ?? fallback?.nilIfBlank
    }

    private static func mergeStrings(preferred: [String], extra: [String]) -> [String] {
        var seen = Set<String>()
        var out: [String] = []
        for v in preferred + extra {
            let n = v.trimmingCharacters(in: .whitespacesAndNewlines)
            if !n.isEmpty && seen.insert(n).inserted { out.append(n) }
        }
        return out
    }
}