Commit 7be2757f for tesseract

commit 7be2757f1ecc67985eb317ed03e5216cc0f9d191
Author: Stefan Weil <sw@weilnetz.de>
Date:   Fri Nov 13 17:30:49 2026 +0100

    lstm: limit the choices per timestep in SaveMostCertainChoices

    The outputs of a softmax add up to 1, so at most 100 of them can reach
    the threshold 0.01. A model whose last layer is not a softmax has no
    such limit. The sorted insertion was quadratic in the number of
    outputs then, and a list of all outputs was kept for each timestep.

    Keep the 100 most likely choices. Their order is unchanged.

    Fix proposed by the Anthropic OSS Scanner (finding ANT-2026-SS02HWH1).

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

diff --git a/src/lstm/recodebeam.cpp b/src/lstm/recodebeam.cpp
index affce775..98949946 100644
--- a/src/lstm/recodebeam.cpp
+++ b/src/lstm/recodebeam.cpp
@@ -137,6 +137,10 @@ void RecodeBeamSearch::DecodeSecondaryBeams(
 void RecodeBeamSearch::SaveMostCertainChoices(const float *outputs,
                                               int num_outputs,
                                               const UNICHARSET *charset) {
+  // The outputs of a softmax add up to 1, so at most 100 of them can reach
+  // 0.01. A model whose last layer is not a softmax has no such limit, and
+  // without a cap the sorted insertion below is quadratic in num_outputs.
+  constexpr size_t kMaxChoices = 100;
   std::vector<std::pair<const char *, float>> choices;
   for (int i = 0; i < num_outputs; ++i) {
     if (outputs[i] >= 0.01f) {
@@ -154,8 +158,14 @@ void RecodeBeamSearch::SaveMostCertainChoices(const float *outputs,
       while (choices.size() > pos && choices[pos].second > outputs[i]) {
         pos++;
       }
+      if (pos >= kMaxChoices) {
+        continue; // less likely than the kMaxChoices choices already kept
+      }
       choices.insert(choices.begin() + pos,
                      std::pair<const char *, float>(character, outputs[i]));
+      if (choices.size() > kMaxChoices) {
+        choices.pop_back();
+      }
     }
   }
   timesteps.push_back(std::move(choices));