Move SSD OCR guess mutation to typed Kotlin

ober

303d60ec36a3ad959affbdd7def1ead6a36044b5

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index c8e11ec..135cf40 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1353,7 +1353,8 @@
         (export make-OcrPattern OcrPattern? OcrPattern-tokens OcrPattern-label
                 OcrPattern-confidence
                 make-OcrCandidate OcrCandidate? OcrCandidate-label
-                OcrCandidate-boxTypeId OcrCandidate-confidence OcrCandidate-reason)
+                OcrCandidate-boxTypeId OcrCandidate-confidence OcrCandidate-reason
+                applyOcrCandidateToGroup)
         (type Float32)
         (record OcrPattern
           ((tokens : (List String))
@@ -1363,7 +1364,27 @@
           ((label : String)
            (boxTypeId : String)
            (confidence : Float32)
-           (reason : String)))))
+           (reason : String)))
+        (def (applyOcrCandidateToGroup (group : SsdGroup)
+                                       (candidate : OcrCandidate)) : Unit
+          (begin
+            (SsdGroup-label-set!
+              group
+              (string-append (OcrCandidate-label candidate) "?"))
+            (if (string-blank? (OcrCandidate-boxTypeId candidate))
+                (begin)
+                (SsdGroup-boxTypeId-set!
+                  group
+                  (OcrCandidate-boxTypeId candidate)))
+            (SsdGroup-notes-set!
+              group
+              (appendNote
+                (SsdGroup-notes group)
+                (string-append
+                  "OCR: "
+                  (string-append
+                    (OcrCandidate-label candidate)
+                    (string-append ", " (OcrCandidate-reason candidate))))))))))
 
     (typed-kotlin-file "com/sfb/ssdreview/OcrStrings.kt"
       (typed-library (com sfb ssdreview)
@@ -4680,9 +4701,7 @@
        "            val text = nearbyText(session.ocrWords, group.bbox, session.imageWidth, session.imageHeight)"
        "            val best = ocrCandidates(context, text, race).firstOrNull() ?: return@forEach"
        "            if (bankCompatible(group.label, group.boxTypeId) && !bankCompatible(best.label, best.boxTypeId)) return@forEach"
-       "            group.label = \"${best.label}?\""
-       "            if (best.boxTypeId.isNotBlank()) group.boxTypeId = best.boxTypeId"
-       "            group.notes = appendNote(group.notes, \"OCR: ${best.label}, ${best.reason}\")"
+       "            applyOcrCandidateToGroup(group, best)"
        "            applied += 1"
        "        }"
        "        return applied"