OcrPostProcessor.swift
94 lignes · 3901 octets
import Foundation enum OcrPostProcessor { private static let emailRegex = KotlinRegex( #"[\w.+-]+@[\w.-]+\.\w{2,}"#, options: [.caseInsensitive] ) private static let urlRegex = KotlinRegex( #"(?:https?://[\w./?#&=%+-]+)|(?:www\.[\w.-]+\.\w{2,}(?:/[\w./?#&=%+-]*)?)"#, options: [.caseInsensitive] ) private static let phoneRegex = KotlinRegex(#"(?:\+?\d[\d\s.\-()]{6,}\d)"#) private static let oBetweenDigits = KotlinRegex(#"(?<=\d)O(?=\d)"#) private static let oBeforeDigit = KotlinRegex(#"\bO(?=\d)"#) static func process(_ rawText: String) -> OcrResult { let lines = rawText.kotlinLines() .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } let joined = lines.joined(separator: "\n") // Line-by-line extraction: avoids absorbing the postal code or street // number of a neighbouring line (\s also matches \n in phoneRegex). let phones = lines // Fix O/0 only inside numeric sequences (frequent OCR errors). .map { oBeforeDigit.replace(oBetweenDigits.replace($0, with: "0"), with: "0") } .flatMap { phoneRegex.findAll($0) } .map { normalizePhone($0) } .filter { isPlausiblePhone($0) } .distinctPreservingOrder() let emails = emailRegex.findAll(joined) .map { $0.lowercased() } .distinctPreservingOrder() let urls = urlRegex.findAll(joined) .map { normalizeUrl($0) } .distinctPreservingOrder() return OcrResult( rawText: joined, lines: lines, phones: phones, emails: emails, urls: urls ) } static func enrich(_ base: OcrResult, rawText: String) -> OcrResult { let extra = process(rawText) return OcrResult( rawText: extra.rawText.isBlank ? base.rawText : extra.rawText, lines: extra.lines.isEmpty ? base.lines : extra.lines, phones: (base.phones + extra.phones).distinctPreservingOrder(), emails: (base.emails + extra.emails).distinctPreservingOrder(), urls: (base.urls + extra.urls).distinctPreservingOrder(), confidence: base.confidence, spatialLines: base.spatialLines ) } static func normalizePhone(_ raw: String) -> String { var s = String(String.UnicodeScalarView(raw.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0) || $0 == "+" })) // +33(0)6... → +336... if s.hasPrefix("+33") && s.count > 4 { let idx = s.index(s.startIndex, offsetBy: 3) if s[idx] == "0" { s = "+33" + s[s.index(after: idx)...] } } if s.hasPrefix("0033") { var rest = String(s.dropFirst(4)) if rest.hasPrefix("0") { rest = String(rest.dropFirst()) } s = "+33" + rest } return s } private static func isPlausiblePhone(_ phone: String) -> Bool { let digits = phone.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0) } // With an international prefix: 11-15 digits. Without: strict FR national // format (10 digits, leading 0) to reject address+phone merges. if phone.hasPrefix("+") { return (11...15).contains(digits.count) } return digits.count == 10 && digits.first == "0" } private static func normalizeUrl(_ raw: String) -> String { var t = raw.trimmingCharacters(in: .whitespacesAndNewlines) while let last = t.last, last == "." || last == "," || last == ";" { t.removeLast() } let lower = t.lowercased() if lower.hasPrefix("http://") || lower.hasPrefix("https://") { return t } return "https://" + t } }
GitRust