import Foundation import SstCore /// Whole-word, case-insensitive, single-pass replacement. Longest match wins, replaced text is never rescanned. public struct CorrectionEngine: TextCorrecting { private struct Rule { let source: String let wrote: String } private let rules: [Rule] private let regex: NSRegularExpression? public init(entries: [DictionaryEntry], knowledge: WordKnowledge) { var seen = Set() var built: [(rule: Rule, order: Int)] = [] for (order, entry) in entries.enumerated() { let source = entry.source.trimmingCharacters(in: .whitespacesAndNewlines) let key = EntryKey.key(source) guard !key.isEmpty, !seen.contains(key) else { continue } // A single-word term that is also a real word only biases the model; capitalising it everywhere would corrupt normal text. if case .term(let term) = entry.kind, EntryKey.parts(term).count == 1, knowledge.isWord(term) { continue } seen.insert(key) built.append((Rule(source: source, wrote: entry.wrote), order)) } let ordered = built.sorted { a, b in let la = a.rule.source.filter(\.isLetter).count let lb = b.rule.source.filter(\.isLetter).count return la != lb ? la > lb : a.order < b.order }.map(\.rule) rules = ordered if ordered.isEmpty { regex = nil } else { let alternatives = ordered.map { "(\(Self.pattern($0.source)))" }.joined(separator: "|") regex = try? NSRegularExpression( pattern: "(? String { EntryKey.parts(source) .map { NSRegularExpression.escapedPattern(for: $0) } .joined(separator: "[\\s\\-‐‑–]*") } public func correct(_ text: String) -> CorrectionResult { guard let regex, !text.isEmpty else { return CorrectionResult(text: text, corrections: []) } let ns = text as NSString var output = "" var outputLength = 0 var last = 0 var applied: [AppliedCorrection] = [] for match in regex.matches(in: text, range: NSRange(location: 0, length: ns.length)) { let gap = ns.substring(with: NSRange(location: last, length: match.range.location - last)) output += gap outputLength += gap.utf16.count let heard = ns.substring(with: match.range) let group = (1..