import CoreGraphics import Foundation struct CardTextHeuristicExtractor { private let numberRegex = try? NSRegularExpression(pattern: #"\b\d{1,3}\s*/\s*\d{1,3}\b"#) private let rarityKeywords: [(needle: String, rarity: CardRarity)] = [ ("special art rare", .specialArtRare), ("illustration rare", .illustrationRare), ("hyper rare", .hyperRare), ("secret rare", .secretRare), ("ultra rare", .ultraRare), ("holo rare", .holoRare), ("uncommon", .uncommon), ("common", .common), ("rare", .rare), ] func extract(payload: OCRTextPayload, source: RecognitionSource, notes: [String] = []) -> CardRecognitionDraft { let cleanedLines = payload.lines .map { line in RecognizedTextLine( text: normalize(line.text), confidence: line.confidence, normalizedBounds: line.normalizedBounds ) } .filter { !$0.text.isEmpty } let rawText = cleanedLines.map(\.text).joined(separator: "\n") let cardNumber = extractCardNumber(from: rawText) let rarity = extractRarity(from: rawText) let cardName = extractCardName(from: cleanedLines) let setIdentifier = extractSetIdentifier(from: cleanedLines, cardNumber: cardNumber, rarity: rarity) let foundCount = [cardName, cardNumber, setIdentifier, rarity == CardRarity.unknown.rawValue ? "" : rarity] .filter { !$0.isEmpty } .count let confidence: ConfidenceLevel if foundCount >= 3 && payload.averageConfidence >= 0.68 { confidence = .high } else if foundCount >= 2 && payload.averageConfidence >= 0.45 { confidence = .medium } else { confidence = .low } var draftNotes = notes if confidence != .high { draftNotes.append(confidence.helperText) } if foundCount == 0 { draftNotes.append("No structured match was found — manual entry is prefilled with OCR fragments.") } return CardRecognitionDraft( cardName: cardName, cardNumber: cardNumber, setIdentifier: setIdentifier, rarity: rarity, source: source, confidence: confidence, notes: Array(Set(draftNotes)), rawText: rawText ) } private func extractCardName(from lines: [RecognizedTextLine]) -> String { let upperLines = lines .filter { $0.normalizedBounds.midY > 0.45 } .sorted { lhs, rhs in if lhs.normalizedBounds.midY == rhs.normalizedBounds.midY { return lhs.confidence > rhs.confidence } return lhs.normalizedBounds.midY > rhs.normalizedBounds.midY } let candidate = upperLines.first { line in let text = line.text return text.rangeOfCharacter(from: .decimalDigits) == nil && !text.localizedCaseInsensitiveContains("hp") && !text.localizedCaseInsensitiveContains("trainer") && text.count > 2 } return candidate?.text ?? lines.first(where: { !$0.text.isEmpty })?.text ?? "" } private func extractCardNumber(from rawText: String) -> String { guard let numberRegex else { return "" } let range = NSRange(rawText.startIndex..., in: rawText) guard let match = numberRegex.firstMatch(in: rawText, options: [], range: range), let matchRange = Range(match.range, in: rawText) else { return "" } return String(rawText[matchRange]).replacingOccurrences(of: " ", with: "") } private func extractSetIdentifier(from lines: [RecognizedTextLine], cardNumber: String, rarity: String) -> String { guard !lines.isEmpty else { return "" } if let lineContainingNumber = lines.first(where: { !cardNumber.isEmpty && $0.text.contains(cardNumber) }) { let stripped = lineContainingNumber.text .replacingOccurrences(of: cardNumber, with: "") .replacingOccurrences(of: "•", with: " ") .replacingOccurrences(of: "·", with: " ") .trimmingCharacters(in: .whitespacesAndNewlines) if stripped.count > 2 { return stripped } } let bottomCandidates = lines .filter { $0.normalizedBounds.midY < 0.35 } .map(\.text) .filter { !$0.localizedCaseInsensitiveContains(rarity) && $0.rangeOfCharacter(from: .letters) != nil && !$0.localizedCaseInsensitiveContains("hp") } return bottomCandidates.first ?? "" } private func extractRarity(from rawText: String) -> String { let lowered = rawText.lowercased() if lowered.contains("★") || lowered.contains("holo") { return CardRarity.holoRare.rawValue } if lowered.contains("◆") { return CardRarity.uncommon.rawValue } if lowered.contains("●") { return CardRarity.common.rawValue } for keyword in rarityKeywords { if lowered.contains(keyword.needle) { return keyword.rarity.rawValue } } return CardRarity.unknown.rawValue } private func normalize(_ value: String) -> String { value .replacingOccurrences(of: " ", with: " ") .trimmingCharacters(in: .whitespacesAndNewlines) } }