Move SSD remote truth session copy to typed Kotlin

ober

48ae7199880fd0e674b14d8239e3cf531246b0a3

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index c1348ff..b023841 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -548,7 +548,8 @@
                 SsdSession-cells SsdSession-groups SsdSession-ocrWords
                 SsdSession-suppressedGroups
                 SsdSession-ssdArea SsdSession-ssdArea-set!
-                ssdSessionOrEmpty makeDetectionSession ssdSessionAreaOrNone
+                ssdSessionOrEmpty makeDetectionSession makeRemoteTruthMergeSession
+                ssdSessionAreaOrNone
                 ssdSessionCellInsideSsdArea ssdSessionGroupInsideSsdArea
                 ssdSessionSuppressedGroupPresent?
                 ssdSessionFindGroupById ssdSessionSelectedGroup
@@ -632,6 +633,28 @@
             (mutable-list-empty OcrWord)
             (mutable-list-empty SsdGroup)
             (nullable-none FloatArray)))
+        (def (copyOcrWords (words : (MutableList OcrWord))) : (MutableList OcrWord)
+          (for/fold ((out (mutable-list-empty OcrWord)))
+                    ((i (in-range (int32 0) (list-size words))))
+            (begin
+              (mutable-list-add! out (list-ref words i))
+              out)))
+        (def (makeRemoteTruthMergeSession (source : SsdSession)) : SsdSession
+          (make-SsdSession
+            (SsdSession-sessionId source)
+            (SsdSession-sourceKey source)
+            (SsdSession-sourceName source)
+            (SsdSession-sourceUri source)
+            (SsdSession-page source)
+            (SsdSession-pageCount source)
+            (SsdSession-dpi source)
+            (SsdSession-imageWidth source)
+            (SsdSession-imageHeight source)
+            (mutable-list-empty SsdCell)
+            (mutable-list-empty SsdGroup)
+            (copyOcrWords (SsdSession-ocrWords source))
+            (mutable-list-empty SsdGroup)
+            (nullable-none FloatArray)))
         (def (ssdSessionAreaOrNone (session : (Nullable SsdSession))) : (Nullable FloatArray)
           (if (nullable-null? session)
             (nullable-none FloatArray)
@@ -7214,12 +7237,7 @@
        "            val truth = truthStore.loadTruth(session)"
        "            if (!truthPresent(truth)) return@thread"
        "            val loadedTruth = truthJsonObjectOrEmpty(truth)"
-       "            val merged = session.copy("
-       "                cells = mutableListOf(),"
-       "                groups = mutableListOf(),"
-       "                suppressedGroups = mutableListOf(),"
-       "                ocrWords = session.ocrWords.toMutableList()"
-       "            )"
+       "            val merged = makeRemoteTruthMergeSession(session)"
        "            merged.ssdArea = truthSsdAreaRect(loadedTruth)"
        "            truthStore.applyTruth(merged)"
        "            ssdSessionPruneGroupsOutsideSsdArea(merged)"