Move SSD OCR nearby text to typed Kotlin

ober

dbe9f642567cf4cdeae95bd9752b896049866553

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index cbd3610..f2cad5e 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -1354,7 +1354,8 @@
 
     (typed-kotlin-file "com/sfb/ssdreview/OcrGeometry.kt"
       (typed-library (com sfb ssdreview)
-        (export centerInside rectArea normalizeRect rectsOverlap distanceToRect)
+        (export floatArrayGetOrZero
+                centerInside rectArea normalizeRect rectsOverlap distanceToRect)
         (type Float32)
         (type FloatArray)
         (type Int32)
@@ -1429,6 +1430,179 @@
                           (float32 0.0)))))
               (+ dx dy))))))
 
+    (typed-kotlin-file "com/sfb/ssdreview/NearbyText.kt"
+      (typed-library (com sfb ssdreview)
+        (export nearbyText)
+        (type Float32)
+        (type FloatArray)
+        (type Int32)
+        (type OcrWord)
+        (record NearbyBest
+          ((mut firstWord : (Nullable OcrWord))
+           (mut firstDistance : Float32)
+           (mut secondWord : (Nullable OcrWord))
+           (mut secondDistance : Float32)
+           (mut thirdWord : (Nullable OcrWord))
+           (mut thirdDistance : Float32)
+           (mut fourthWord : (Nullable OcrWord))
+           (mut fourthDistance : Float32)))
+        (def (nearbyMaxFloat32 (a : Float32) (b : Float32)) : Float32
+          (if (> a b) a b))
+        (def (nearbyMinFloat32 (a : Float32) (b : Float32)) : Float32
+          (if (< a b) a b))
+        (def (nearbyAppend (text : String) (piece : String)) : String
+          (if (string-blank? text)
+              piece
+              (string-append text (string-append " " piece))))
+        (def (nearbyWordMatches? (word : OcrWord) (rect : FloatArray)) : Bool
+          (or (rectsOverlap (OcrWord-bbox word) rect)
+              (centerInside (OcrWord-bbox word) rect)))
+        (def (nearbyDirectCount (words : (List OcrWord))
+                                (rect : FloatArray)) : Int32
+          (for/fold ((count (int32 0)))
+                    ((i (in-range (int32 0) (list-size words))))
+            (if (nearbyWordMatches? (list-ref words i) rect)
+                (+ count (int32 1))
+                count)))
+        (def (nearbyDirectText (words : (List OcrWord))
+                               (rect : FloatArray)) : String
+          (string-take
+            (for/fold ((text ""))
+                      ((i (in-range (int32 0) (list-size words))))
+              (let ((word (list-ref words i)))
+                (if (nearbyWordMatches? word rect)
+                    (nearbyAppend text (OcrWord-text word))
+                    text)))
+            (int32 240)))
+        (def (nearbyEmptyBest) : NearbyBest
+          (make-NearbyBest
+            (nullable-none OcrWord) (float32 0.0)
+            (nullable-none OcrWord) (float32 0.0)
+            (nullable-none OcrWord) (float32 0.0)
+            (nullable-none OcrWord) (float32 0.0)))
+        (def (nearbySetFourth (best : NearbyBest)
+                              (word : (Nullable OcrWord))
+                              (distance : Float32)) : Unit
+          (begin
+            (NearbyBest-fourthWord-set! best word)
+            (NearbyBest-fourthDistance-set! best distance)))
+        (def (nearbySetThird (best : NearbyBest)
+                             (word : (Nullable OcrWord))
+                             (distance : Float32)) : Unit
+          (begin
+            (NearbyBest-thirdWord-set! best word)
+            (NearbyBest-thirdDistance-set! best distance)))
+        (def (nearbySetSecond (best : NearbyBest)
+                              (word : (Nullable OcrWord))
+                              (distance : Float32)) : Unit
+          (begin
+            (NearbyBest-secondWord-set! best word)
+            (NearbyBest-secondDistance-set! best distance)))
+        (def (nearbySetFirst (best : NearbyBest)
+                             (word : (Nullable OcrWord))
+                             (distance : Float32)) : Unit
+          (begin
+            (NearbyBest-firstWord-set! best word)
+            (NearbyBest-firstDistance-set! best distance)))
+        (def (nearbyInsertFirst (best : NearbyBest)
+                                (word : OcrWord)
+                                (distance : Float32)) : Unit
+          (begin
+            (nearbySetFourth best (NearbyBest-thirdWord best) (NearbyBest-thirdDistance best))
+            (nearbySetThird best (NearbyBest-secondWord best) (NearbyBest-secondDistance best))
+            (nearbySetSecond best (NearbyBest-firstWord best) (NearbyBest-firstDistance best))
+            (nearbySetFirst best (nullable-some word) distance)))
+        (def (nearbyInsertSecond (best : NearbyBest)
+                                 (word : OcrWord)
+                                 (distance : Float32)) : Unit
+          (begin
+            (nearbySetFourth best (NearbyBest-thirdWord best) (NearbyBest-thirdDistance best))
+            (nearbySetThird best (NearbyBest-secondWord best) (NearbyBest-secondDistance best))
+            (nearbySetSecond best (nullable-some word) distance)))
+        (def (nearbyInsertThird (best : NearbyBest)
+                                (word : OcrWord)
+                                (distance : Float32)) : Unit
+          (begin
+            (nearbySetFourth best (NearbyBest-thirdWord best) (NearbyBest-thirdDistance best))
+            (nearbySetThird best (nullable-some word) distance)))
+        (def (nearbyInsertFourth (best : NearbyBest)
+                                 (word : OcrWord)
+                                 (distance : Float32)) : Unit
+          (nearbySetFourth best (nullable-some word) distance))
+        (def (nearbyMaybeInsert (best : NearbyBest)
+                                (word : OcrWord)
+                                (distance : Float32)) : Unit
+          (if (or (nullable-null? (NearbyBest-firstWord best))
+                  (< distance (NearbyBest-firstDistance best)))
+              (nearbyInsertFirst best word distance)
+              (if (or (nullable-null? (NearbyBest-secondWord best))
+                      (< distance (NearbyBest-secondDistance best)))
+                  (nearbyInsertSecond best word distance)
+                  (if (or (nullable-null? (NearbyBest-thirdWord best))
+                          (< distance (NearbyBest-thirdDistance best)))
+                      (nearbyInsertThird best word distance)
+                      (if (or (nullable-null? (NearbyBest-fourthWord best))
+                              (< distance (NearbyBest-fourthDistance best)))
+                          (nearbyInsertFourth best word distance)
+                          (begin))))))
+        (def (nearbyCollectBest (words : (List OcrWord))
+                                (cx : Float32)
+                                (cy : Float32)
+                                (limit : Float32)) : NearbyBest
+          (let ((best (nearbyEmptyBest)))
+            (begin
+              (for/fold ((ignored (int32 0)))
+                        ((i (in-range (int32 0) (list-size words))))
+                (let ((word (list-ref words i)))
+                  (let ((distance (distanceToRect cx cy (OcrWord-bbox word))))
+                    (begin
+                      (if (<= distance limit)
+                          (nearbyMaybeInsert best word distance)
+                          (begin))
+                      ignored))))
+              best)))
+        (def (nearbyAppendWord (text : String)
+                               (word : (Nullable OcrWord))) : String
+          (if (nullable-null? word)
+              text
+              (nearbyAppend text (OcrWord-text (nullable-get word)))))
+        (def (nearbyBestText (best : NearbyBest)) : String
+          (string-take
+            (nearbyAppendWord
+              (nearbyAppendWord
+                (nearbyAppendWord
+                  (nearbyAppendWord "" (NearbyBest-firstWord best))
+                  (NearbyBest-secondWord best))
+                (NearbyBest-thirdWord best))
+              (NearbyBest-fourthWord best))
+            (int32 240)))
+        (def (nearbyText (words : (List OcrWord))
+                         (bbox : FloatArray)
+                         (imageWidth : Int32)
+                         (imageHeight : Int32)) : String
+          (let ((x1 (floatArrayGetOrZero bbox (int32 0)))
+                (y1 (floatArrayGetOrZero bbox (int32 1)))
+                (x2 (floatArrayGetOrZero bbox (int32 2)))
+                (y2 (floatArrayGetOrZero bbox (int32 3))))
+            (let ((padX (nearbyMaxFloat32 (float32 90.0) (* (- x2 x1) (float32 3.0))))
+                  (padY (nearbyMaxFloat32 (float32 80.0) (* (- y2 y1) (float32 2.2)))))
+              (let ((rect
+                      (float-array
+                        (nearbyMaxFloat32 (float32 0.0) (- x1 padX))
+                        (nearbyMaxFloat32 (float32 0.0) (- y1 padY))
+                        (nearbyMinFloat32 (float32 imageWidth) (+ x2 padX))
+                        (nearbyMinFloat32 (float32 imageHeight) (+ y2 padY)))))
+                (if (> (nearbyDirectCount words rect) (int32 0))
+                    (nearbyDirectText words rect)
+                    (let ((cx (/ (+ x1 x2) (float32 2.0)))
+                          (cy (/ (+ y1 y2) (float32 2.0))))
+                      (nearbyBestText
+                        (nearbyCollectBest
+                          words
+                          cx
+                          cy
+                          (* (nearbyMaxFloat32 padX padY) (float32 1.5))))))))))))
+
     (typed-kotlin-file "com/sfb/ssdreview/VisionPixels.kt"
       (typed-library (com sfb ssdreview)
         (export grayscale pixelsToRgba isColorMask looksLikeInk looksLikeBoxFill)
@@ -4290,8 +4464,6 @@
        "import com.google.mlkit.vision.common.InputImage"
        "import com.google.mlkit.vision.text.TextRecognition"
        "import com.google.mlkit.vision.text.latin.TextRecognizerOptions"
-       "import kotlin.math.max"
-       "import kotlin.math.min"
        ""
        "object SsdOcr {"
        "    private val patterns = listOf("
@@ -4374,24 +4546,6 @@
        "        return applied"
        "    }"
        ""
-       "    private fun nearbyText(words: List<OcrWord>, bbox: FloatArray, imageWidth: Int, imageHeight: Int): String {"
-       "        val x1 = bbox.getOrElse(0) { 0f }"
-       "        val y1 = bbox.getOrElse(1) { 0f }"
-       "        val x2 = bbox.getOrElse(2) { 0f }"
-       "        val y2 = bbox.getOrElse(3) { 0f }"
-       "        val padX = max(45f, (x2 - x1) * 1.8f)"
-       "        val padY = max(35f, (y2 - y1) * 1.2f)"
-       "        val rect = floatArrayOf("
-       "            max(0f, x1 - padX),"
-       "            max(0f, y1 - padY),"
-       "            min(imageWidth.toFloat(), x2 + padX),"
-       "            min(imageHeight.toFloat(), y2 + padY)"
-       "        )"
-       "        return words.filter { centerInside(it.bbox, rect) }"
-       "            .joinToString(\" \") { it.text }"
-       "            .take(240)"
-       "    }"
-       ""
        "    private fun ocrCandidates(context: Context, text: String, race: String): List<OcrCandidate> {"
        "        if (text.isBlank()) return emptyList()"
        "        val seen = mutableSetOf<String>()"