Treat SSD area truth as detector boundary

ober

96a4b7772389cf87f2caeb286f2bc9fb2ee66b17

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index fd06b41..30e9e02 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -298,6 +298,7 @@
        "import kotlin.system.exitProcess"
        "import kotlin.math.ceil"
        "import kotlin.math.max"
+       "import kotlin.math.min"
        "import kotlin.math.sqrt"
        ""
        "class MainActivity : Activity() {"
@@ -774,19 +775,36 @@
 	       "            setStatus(\"SSD area ignored: selection was too small\")"
 	       "            return"
 	       "        }"
-	       "        val removed = session.setSsdArea(area)"
+	       "        session.ssdArea = floatArrayOf("
+	       "            min(area[0], area[2]),"
+	       "            min(area[1], area[3]),"
+	       "            max(area[0], area[2]),"
+	       "            max(area[1], area[3])"
+	       "        )"
+	       "        val beforeGroups = session.groups.size"
+	       "        val outsideCount = session.groups.count { !session.groupInsideSsdArea(it) }"
+	       "        val removed = if (beforeGroups > 0 && outsideCount < beforeGroups) {"
+	       "            session.pruneGroupsOutsideSsdArea()"
+	       "        } else {"
+	       "            0"
+	       "        }"
 	       "        selectedGroupId = session.groups.firstOrNull { it.id == selectedGroupId }?.id"
 	       "            ?: session.groups.firstOrNull()?.id"
 	       "        reviewView.ssdArea = session.ssdArea"
 	       "        reviewView.selectedGroupId = selectedGroupId"
-	       "        truthStore.appendEvent(\"ssd_area_set\", session, JSONObject().put(\"removed_groups\", removed))"
+	       "        truthStore.appendEvent(\"ssd_area_set\", session, JSONObject()"
+	       "            .put(\"removed_groups\", removed)"
+	       "            .put(\"before_groups\", beforeGroups)"
+	       "            .put(\"outside_groups\", outsideCount))"
 	       "        reviewView.performHapticFeedback(HapticFeedbackConstants.CONFIRM)"
 	       "        reviewView.invalidate()"
 	       "        saveSessionAsync("
 	       "            session,"
-	       "            \"SSD area set; removed $removed outside tags; saving...\","
-	       "            \"SSD area saved; removed $removed outside tags\""
-	       "        )"
+	       "            \"SSD area set; saving boundary...\","
+	       "            \"SSD area saved; detecting inside boundary...\""
+	       "        ) {"
+	       "            detectCurrent(forceDetect = true)"
+	       "        }"
 	       "    }"
 	       ""
 	       "    private fun selectGroup(groupId: String?) {"
@@ -929,44 +947,42 @@
        "                }"
        ""
        "                val remoteMatches = 0"
-       "                val existingTruth = truthStore.loadTruth(session)"
+       "                val existingTruth = localTruth ?: truthStore.loadTruth(session)"
+       "                val existingTruthGroupCount = existingTruth?.optJSONArray(\"groups\")?.length() ?: 0"
        "                var ocrGuesses = 0"
        "                var detectorNote = \"\""
        "                var detectorSource = \"unknown\""
        ""
-	       "                if (existingTruth != null) {"
+	       "                if (existingTruth != null && existingTruthGroupCount > 0 && !forceDetect) {"
 	       "                    detectorSource = \"local-truth\""
 	       "                    session.ssdArea = jsonRect(existingTruth.optJSONArray(\"ssd_area\"))"
-	       "                    if (forceDetect) {"
-       "                        val forcedDetection = SsdVisionBridge.detect(bitmap)"
-       "                        if (forcedDetection != null) {"
-       "                            detectorNote = \", rust detector\""
-       "                            detectorSource = \"forced-rust-plus-truth\""
-       "                        } else {"
-       "                            detectorSource = \"forced-android-plus-truth\""
-       "                        }"
-       "                        val forcedCells = forcedDetection?.cells ?: SsdDetector.detectCells(bitmap)"
-       "                        session.cells.addAll(forcedCells.filter { session.cellInsideSsdArea(it) })"
-	       "                    }"
 	       "                    truthStore.applyTruth(session)"
 	       "                    session.pruneGroupsOutsideSsdArea()"
        "                } else {"
+       "                    if (existingTruth != null) {"
+       "                        session.ssdArea = jsonRect(existingTruth.optJSONArray(\"ssd_area\"))"
+       "                    }"
        "                    val nativeDetection = SsdVisionBridge.detect(bitmap)"
+       "                    val detectedCells: List<SsdCell>"
        "                    if (nativeDetection != null) {"
-       "                        session.cells.addAll(nativeDetection.cells)"
-       "                        session.groups.addAll(nativeDetection.groups.take(80))"
+       "                        detectedCells = nativeDetection.cells"
        "                        detectorNote = \", rust detector\""
-       "                        detectorSource = \"rust-detector\""
+       "                        detectorSource = if (forceDetect && existingTruthGroupCount > 0) \"forced-rust-plus-truth\" else \"rust-detector\""
        "                    } else {"
-       "                        session.cells.addAll(SsdDetector.detectCells(bitmap))"
-       "                        session.groups.addAll(SsdDetector.initialGroups(session.cells).take(80))"
-       "                        detectorSource = \"android-fallback\""
+       "                        detectedCells = SsdDetector.detectCells(bitmap)"
+       "                        detectorSource = if (forceDetect && existingTruthGroupCount > 0) \"forced-android-plus-truth\" else \"android-fallback\""
+       "                    }"
+       "                    session.cells.addAll(detectedCells.filter { session.cellInsideSsdArea(it) })"
+       "                    session.groups.addAll(SsdDetector.initialGroups(session.cells).take(120))"
+       "                    if (existingTruth != null && existingTruthGroupCount > 0) {"
+       "                        truthStore.applyTruth(session)"
+       "                        session.pruneGroupsOutsideSsdArea()"
        "                    }"
        "                    Guessing.applyInitialGuesses(this, session)"
        "                    session.ocrWords.addAll(SsdOcr.recognizeWords(bitmap))"
        "                    ocrGuesses = SsdOcr.applyOcrGuesses(this, session)"
        "                }"
-       "                val learnedGuesses = if (existingTruth == null) truthStore.applyLearnedGuesses(session) else 0"
+       "                val learnedGuesses = if (existingTruth == null || existingTruthGroupCount == 0) truthStore.applyLearnedGuesses(session) else 0"
        "                runOnUiThread {"
        "                    if (!isCurrentLoad(expectedGeneration, uri, expectedPageIndex)) return@runOnUiThread"
        "                    currentSession = session"
@@ -1138,7 +1154,12 @@
        "        count.requestFocus()"
        "    }"
        ""
-       "    private fun saveSessionAsync(session: SsdSession, savingStatus: String, savedStatus: String) {"
+       "    private fun saveSessionAsync("
+       "        session: SsdSession,"
+       "        savingStatus: String,"
+       "        savedStatus: String,"
+       "        afterSaved: (() -> Unit)? = null"
+       "    ) {"
        "        val truth = session.toTruthJson()"
        "        val viewport = reviewView.captureViewport()"
        "        finishSelectInteraction()"
@@ -1150,6 +1171,7 @@
        "                runOnUiThread {"
        "                    restoreViewportAfterLayout(viewport)"
        "                    setStatus(savedStatus)"
+       "                    afterSaved?.invoke()"
        "                }"
        "            } catch (error: Exception) {"
        "                runOnUiThread {"
@@ -2115,6 +2137,7 @@
 	       "    fun pruneGroupsOutsideSsdArea(): Int {"
 	       "        val area = ssdArea ?: return 0"
 	       "        val outside = groups.filter { group -> !rectCenterInside(group.bbox, area) }.toList()"
+	       "        if (outside.isNotEmpty() && outside.size >= groups.size) return 0"
 	       "        outside.forEach { group ->"
 	       "            groups.remove(group)"
 	       "            group.status = \"suppressed\""