Commit 4e3ec57f for tesseract

commit 4e3ec57f3039fd09e183f6884b8e1b70a57e9b0f
Author: Stefan Weil <sw@weilnetz.de>
Date:   Tue Nov 10 19:49:39 2026 +0100

    wordrec: stop the diacritics reassignment when the budget is used up

    ReassignDiacritics() only checks the classification budget before it
    starts, but it classifies the noise outlines with many trial words
    (ClassifyBlobAsWord() via SelectGoodDiacriticOutlines()), and the budget
    is enforced between words. When the budget is used up during the trials,
    the remaining trials get the fake result of a skipped word, whose raw
    choice has certainty -1 (and a certainty of -0.1 for a new blob, where
    the negated squared certainty over the rating is used). That beats the
    thresholds noise_cert_basechar (-8), noise_cert_punc (-3) and
    noise_cert_disjoint (-1), so unclassified noise would be added to the
    word as diacritics.

    Check the budget in SelectGoodDiacriticOutlines() before every trial
    and give up without changing the outlines when no language can classify
    any more. Decisions which were already made with real classifications
    are kept.

    Assisted-by: Claude Fable 5.1 (Anthropic)
    Signed-off-by: Stefan Weil <sw@weilnetz.de>

diff --git a/src/ccmain/control.cpp b/src/ccmain/control.cpp
index c11f1650..235186fd 100644
--- a/src/ccmain/control.cpp
+++ b/src/ccmain/control.cpp
@@ -1182,6 +1182,13 @@ bool Tesseract::SelectGoodDiacriticOutlines(int pass, float certainty_threshold,
                                             C_BLOB *blob,
                                             const std::vector<C_OUTLINE *> &outlines,
                                             int num_outlines, std::vector<bool> *ok_outlines) {
+  // The classification budget is enforced between words, so once it is used
+  // up, ClassifyBlobAsWord() returns the certainty of a fake result, which
+  // would beat the threshold. Give up before every probe when no language can
+  // classify any more, without touching the outlines.
+  if (AllClassificationBudgetsExhausted()) {
+    return false;
+  }
   float target_cert = certainty_threshold;
   if (blob != nullptr) {
     std::string best_str;
@@ -1193,6 +1200,9 @@ bool Tesseract::SelectGoodDiacriticOutlines(int pass, float certainty_threshold,
       blob->bounding_box().print();
     }
     target_cert -= (target_cert - certainty_threshold) * noise_cert_factor;
+    if (AllClassificationBudgetsExhausted()) {
+      return false;
+    }
   }
   std::vector<bool> test_outlines = *ok_outlines;
   // Start with all the outlines in.
@@ -1218,6 +1228,9 @@ bool Tesseract::SelectGoodDiacriticOutlines(int pass, float certainty_threshold,
     best_index = -1;
     for (unsigned i = 0; i < outlines.size(); ++i) {
       if (test_outlines[i]) {
+        if (AllClassificationBudgetsExhausted()) {
+          return false;
+        }
         test_outlines[i] = false;
         std::string str;
         float cert = ClassifyBlobPlusOutlines(test_outlines, outlines, pass, pr_it, blob, str);