diff mupdf-source/thirdparty/tesseract/src/textord/makerow.cpp @ 2:b50eed0cc0ef upstream

ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4. The directory name has changed: no version number in the expanded directory now.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:43:07 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mupdf-source/thirdparty/tesseract/src/textord/makerow.cpp	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,2603 @@
+/**********************************************************************
+ * File:        makerow.cpp  (Formerly makerows.c)
+ * Description: Code to arrange blobs into rows of text.
+ * Author:      Ray Smith
+ *
+ * (C) Copyright 1992, Hewlett-Packard Ltd.
+ ** Licensed under the Apache License, Version 2.0 (the "License");
+ ** you may not use this file except in compliance with the License.
+ ** You may obtain a copy of the License at
+ ** http://www.apache.org/licenses/LICENSE-2.0
+ ** Unless required by applicable law or agreed to in writing, software
+ ** distributed under the License is distributed on an "AS IS" BASIS,
+ ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ** See the License for the specific language governing permissions and
+ ** limitations under the License.
+ *
+ **********************************************************************/
+
+// Include automatically generated configuration file if running autoconf.
+#ifdef HAVE_CONFIG_H
+#  include "config_auto.h"
+#endif
+
+#include "makerow.h"
+
+#include "blkocc.h"
+#include "blobbox.h"
+#include "ccstruct.h"
+#include "detlinefit.h"
+#include "drawtord.h"
+#include "oldbasel.h"
+#include "sortflts.h"
+#include "statistc.h"
+#include "textord.h"
+#include "tordmain.h"
+#include "tovars.h"
+#include "tprintf.h"
+#include "underlin.h"
+
+#include <algorithm>
+#include <cmath>
+#include <vector> // for std::vector
+
+namespace tesseract {
+
+BOOL_VAR(textord_heavy_nr, false, "Vigorously remove noise");
+BOOL_VAR(textord_show_initial_rows, false, "Display row accumulation");
+BOOL_VAR(textord_show_parallel_rows, false, "Display page correlated rows");
+BOOL_VAR(textord_show_expanded_rows, false, "Display rows after expanding");
+BOOL_VAR(textord_show_final_rows, false, "Display rows after final fitting");
+BOOL_VAR(textord_show_final_blobs, false, "Display blob bounds after pre-ass");
+BOOL_VAR(textord_test_landscape, false, "Tests refer to land/port");
+BOOL_VAR(textord_parallel_baselines, true, "Force parallel baselines");
+BOOL_VAR(textord_straight_baselines, false, "Force straight baselines");
+BOOL_VAR(textord_old_baselines, true, "Use old baseline algorithm");
+BOOL_VAR(textord_old_xheight, false, "Use old xheight algorithm");
+BOOL_VAR(textord_fix_xheight_bug, true, "Use spline baseline");
+BOOL_VAR(textord_fix_makerow_bug, true, "Prevent multiple baselines");
+BOOL_VAR(textord_debug_xheights, false, "Test xheight algorithms");
+static BOOL_VAR(textord_biased_skewcalc, true, "Bias skew estimates with line length");
+static BOOL_VAR(textord_interpolating_skew, true, "Interpolate across gaps");
+static INT_VAR(textord_skewsmooth_offset, 4, "For smooth factor");
+static INT_VAR(textord_skewsmooth_offset2, 1, "For smooth factor");
+INT_VAR(textord_test_x, -INT32_MAX, "coord of test pt");
+INT_VAR(textord_test_y, -INT32_MAX, "coord of test pt");
+INT_VAR(textord_min_blobs_in_row, 4, "Min blobs before gradient counted");
+INT_VAR(textord_spline_minblobs, 8, "Min blobs in each spline segment");
+INT_VAR(textord_spline_medianwin, 6, "Size of window for spline segmentation");
+static INT_VAR(textord_max_blob_overlaps, 4, "Max number of blobs a big blob can overlap");
+INT_VAR(textord_min_xheight, 10, "Min credible pixel xheight");
+double_VAR(textord_spline_shift_fraction, 0.02, "Fraction of line spacing for quad");
+double_VAR(textord_skew_ile, 0.5, "Ile of gradients for page skew");
+double_VAR(textord_skew_lag, 0.02, "Lag for skew on row accumulation");
+double_VAR(textord_linespace_iqrlimit, 0.2, "Max iqr/median for linespace");
+double_VAR(textord_width_limit, 8, "Max width of blobs to make rows");
+double_VAR(textord_chop_width, 1.5, "Max width before chopping");
+static double_VAR(textord_expansion_factor, 1.0, "Factor to expand rows by in expand_rows");
+static double_VAR(textord_overlap_x, 0.375, "Fraction of linespace for good overlap");
+double_VAR(textord_minxh, 0.25, "fraction of linesize for min xheight");
+double_VAR(textord_min_linesize, 1.25, "* blob height for initial linesize");
+double_VAR(textord_excess_blobsize, 1.3, "New row made if blob makes row this big");
+double_VAR(textord_occupancy_threshold, 0.4, "Fraction of neighbourhood");
+double_VAR(textord_underline_width, 2.0, "Multiple of line_size for underline");
+double_VAR(textord_min_blob_height_fraction, 0.75,
+           "Min blob height/top to include blob top into xheight stats");
+double_VAR(textord_xheight_mode_fraction, 0.4, "Min pile height to make xheight");
+double_VAR(textord_ascheight_mode_fraction, 0.08, "Min pile height to make ascheight");
+static double_VAR(textord_descheight_mode_fraction, 0.08, "Min pile height to make descheight");
+double_VAR(textord_ascx_ratio_min, 1.25, "Min cap/xheight");
+double_VAR(textord_ascx_ratio_max, 1.8, "Max cap/xheight");
+double_VAR(textord_descx_ratio_min, 0.25, "Min desc/xheight");
+double_VAR(textord_descx_ratio_max, 0.6, "Max desc/xheight");
+double_VAR(textord_xheight_error_margin, 0.1, "Accepted variation");
+INT_VAR(textord_lms_line_trials, 12, "Number of linew fits to do");
+BOOL_VAR(textord_new_initial_xheight, true, "Use test xheight mechanism");
+BOOL_VAR(textord_debug_blob, false, "Print test blob information");
+
+#define MAX_HEIGHT_MODES 12
+
+const int kMinLeaderCount = 5;
+
+/**
+ * @name row_y_order
+ *
+ * Sort function to sort rows in y from page top.
+ */
+static int row_y_order(       // sort function
+    const void *item1, // items to compare
+    const void *item2) {
+  // converted ptr
+  const TO_ROW *row1 = *reinterpret_cast<const TO_ROW *const *>(item1);
+  // converted ptr
+  const TO_ROW *row2 = *reinterpret_cast<const TO_ROW *const *>(item2);
+
+  if (row1->parallel_c() > row2->parallel_c()) {
+    return -1;
+  } else if (row1->parallel_c() < row2->parallel_c()) {
+    return 1;
+  } else {
+    return 0;
+  }
+}
+
+/**
+ * @name row_spacing_order
+ *
+ * Qsort style function to compare 2 TO_ROWS based on their spacing value.
+ */
+static int row_spacing_order( // sort function
+    const TO_ROW *row1, // items to compare
+    const TO_ROW *row2) {
+  return row1->spacing < row2->spacing;
+}
+
+// Factored-out helper to build a single row from a list of blobs.
+// Returns the mean blob size.
+static float MakeRowFromBlobs(float line_size, BLOBNBOX_IT *blob_it, TO_ROW_IT *row_it) {
+  blob_it->sort(blob_x_order);
+  blob_it->move_to_first();
+  TO_ROW *row = nullptr;
+  float total_size = 0.0f;
+  int blob_count = 0;
+  // Add all the blobs to a single TO_ROW.
+  for (; !blob_it->empty(); blob_it->forward()) {
+    BLOBNBOX *blob = blob_it->extract();
+    int top = blob->bounding_box().top();
+    int bottom = blob->bounding_box().bottom();
+    if (row == nullptr) {
+      row = new TO_ROW(blob, top, bottom, line_size);
+      row_it->add_before_then_move(row);
+    } else {
+      row->add_blob(blob, top, bottom, line_size);
+    }
+    total_size += top - bottom;
+    ++blob_count;
+  }
+  return blob_count > 0 ? total_size / blob_count : total_size;
+}
+
+// Helper to make a row using the children of a single blob.
+// Returns the mean size of the blobs created.
+static float MakeRowFromSubBlobs(TO_BLOCK *block, C_BLOB *blob, TO_ROW_IT *row_it) {
+  // The blobs made from the children will go in the small_blobs list.
+  BLOBNBOX_IT bb_it(&block->small_blobs);
+  C_OUTLINE_IT ol_it(blob->out_list());
+  // Get the children.
+  ol_it.set_to_list(ol_it.data()->child());
+  if (ol_it.empty()) {
+    return 0.0f;
+  }
+  for (ol_it.mark_cycle_pt(); !ol_it.cycled_list(); ol_it.forward()) {
+    // Deep copy the child outline and use that to make a blob.
+    blob = new C_BLOB(C_OUTLINE::deep_copy(ol_it.data()));
+    // Correct direction as needed.
+    blob->CheckInverseFlagAndDirection();
+    auto *bbox = new BLOBNBOX(blob);
+    bb_it.add_after_then_move(bbox);
+  }
+  // Now we can make a row from the blobs.
+  return MakeRowFromBlobs(block->line_size, &bb_it, row_it);
+}
+
+/**
+ * @name make_single_row
+ *
+ * Arrange the blobs into a single row... well actually, if there is
+ * only a single blob, it makes 2 rows, in case the top-level blob
+ * is a container of the real blobs to recognize.
+ */
+float make_single_row(ICOORD page_tr, bool allow_sub_blobs, TO_BLOCK *block,
+                      TO_BLOCK_LIST *blocks) {
+  BLOBNBOX_IT blob_it = &block->blobs;
+  TO_ROW_IT row_it = block->get_rows();
+
+  // Include all the small blobs and large blobs.
+  blob_it.add_list_after(&block->small_blobs);
+  blob_it.add_list_after(&block->noise_blobs);
+  blob_it.add_list_after(&block->large_blobs);
+  if (block->blobs.singleton() && allow_sub_blobs) {
+    blob_it.move_to_first();
+    float size = MakeRowFromSubBlobs(block, blob_it.data()->cblob(), &row_it);
+    if (size > block->line_size) {
+      block->line_size = size;
+    }
+  } else if (block->blobs.empty()) {
+    // Make a fake blob.
+    C_BLOB *blob = C_BLOB::FakeBlob(block->block->pdblk.bounding_box());
+    // The blobnbox owns the blob.
+    auto *bblob = new BLOBNBOX(blob);
+    blob_it.add_after_then_move(bblob);
+  }
+  MakeRowFromBlobs(block->line_size, &blob_it, &row_it);
+  // Fit an LMS line to the rows.
+  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+    fit_lms_line(row_it.data());
+  }
+  float gradient;
+  float fit_error;
+  // Compute the skew based on the fitted line.
+  compute_page_skew(blocks, gradient, fit_error);
+  return gradient;
+}
+
+/**
+ * @name make_rows
+ *
+ * Arrange the blobs into rows.
+ */
+float make_rows(ICOORD page_tr, TO_BLOCK_LIST *port_blocks) {
+  float port_m;         // global skew
+  float port_err;       // global noise
+  TO_BLOCK_IT block_it; // iterator
+
+  block_it.set_to_list(port_blocks);
+  for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
+    make_initial_textrows(page_tr, block_it.data(), FCOORD(1.0f, 0.0f), !textord_test_landscape);
+  }
+  // compute globally
+  compute_page_skew(port_blocks, port_m, port_err);
+  block_it.set_to_list(port_blocks);
+  for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
+    cleanup_rows_making(page_tr, block_it.data(), port_m, FCOORD(1.0f, 0.0f),
+                        block_it.data()->block->pdblk.bounding_box().left(),
+                        !textord_test_landscape);
+  }
+  return port_m; // global skew
+}
+
+/**
+ * @name make_initial_textrows
+ *
+ * Arrange the good blobs into rows of text.
+ */
+void make_initial_textrows( // find lines
+    ICOORD page_tr,
+    TO_BLOCK *block, // block to do
+    FCOORD rotation, // for drawing
+    bool testing_on  // correct orientation
+) {
+  TO_ROW_IT row_it = block->get_rows();
+
+#ifndef GRAPHICS_DISABLED
+  ScrollView::Color colour; // of row
+
+  if (textord_show_initial_rows && testing_on) {
+    if (to_win == nullptr) {
+      create_to_win(page_tr);
+    }
+  }
+#endif
+  // guess skew
+  assign_blobs_to_rows(block, nullptr, 0, true, true, textord_show_initial_rows && testing_on);
+  row_it.move_to_first();
+  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+    fit_lms_line(row_it.data());
+  }
+#ifndef GRAPHICS_DISABLED
+  if (textord_show_initial_rows && testing_on) {
+    colour = ScrollView::RED;
+    for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+      plot_to_row(row_it.data(), colour, rotation);
+      colour = static_cast<ScrollView::Color>(colour + 1);
+      if (colour > ScrollView::MAGENTA) {
+        colour = ScrollView::RED;
+      }
+    }
+  }
+#endif
+}
+
+/**
+ * @name fit_lms_line
+ *
+ * Fit an LMS line to a row.
+ */
+void fit_lms_line(TO_ROW *row) {
+  float m, c; // fitted line
+  tesseract::DetLineFit lms;
+  BLOBNBOX_IT blob_it = row->blob_list();
+
+  for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
+    const TBOX &box = blob_it.data()->bounding_box();
+    lms.Add(ICOORD((box.left() + box.right()) / 2, box.bottom()));
+  }
+  double error = lms.Fit(&m, &c);
+  row->set_line(m, c, error);
+}
+
+/**
+ * @name compute_page_skew
+ *
+ * Compute the skew over a full page by averaging the gradients over
+ * all the lines. Get the error of the same row.
+ */
+void compute_page_skew(    // get average gradient
+    TO_BLOCK_LIST *blocks, // list of blocks
+    float &page_m,         // average gradient
+    float &page_err        // average error
+) {
+  int32_t row_count;             // total rows
+  int32_t blob_count;            // total_blobs
+  int32_t row_err;               // integer error
+  int32_t row_index;             // of total
+  TO_ROW *row;                   // current row
+  TO_BLOCK_IT block_it = blocks; // iterator
+
+  row_count = 0;
+  blob_count = 0;
+  for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
+    POLY_BLOCK *pb = block_it.data()->block->pdblk.poly_block();
+    if (pb != nullptr && !pb->IsText()) {
+      continue; // Pretend non-text blocks don't exist.
+    }
+    row_count += block_it.data()->get_rows()->length();
+    // count up rows
+    TO_ROW_IT row_it(block_it.data()->get_rows());
+    for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+      blob_count += row_it.data()->blob_list()->length();
+    }
+  }
+  if (row_count == 0) {
+    page_m = 0.0f;
+    page_err = 0.0f;
+    return;
+  }
+  // of rows
+  std::vector<float> gradients(blob_count);
+  // of rows
+  std::vector<float> errors(blob_count);
+
+  row_index = 0;
+  for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
+    POLY_BLOCK *pb = block_it.data()->block->pdblk.poly_block();
+    if (pb != nullptr && !pb->IsText()) {
+      continue; // Pretend non-text blocks don't exist.
+    }
+    TO_ROW_IT row_it(block_it.data()->get_rows());
+    for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+      row = row_it.data();
+      blob_count = row->blob_list()->length();
+      row_err = static_cast<int32_t>(std::ceil(row->line_error()));
+      if (row_err <= 0) {
+        row_err = 1;
+      }
+      if (textord_biased_skewcalc) {
+        blob_count /= row_err;
+        for (blob_count /= row_err; blob_count > 0; blob_count--) {
+          gradients[row_index] = row->line_m();
+          errors[row_index] = row->line_error();
+          row_index++;
+        }
+      } else if (blob_count >= textord_min_blobs_in_row) {
+        // get gradient
+        gradients[row_index] = row->line_m();
+        errors[row_index] = row->line_error();
+        row_index++;
+      }
+    }
+  }
+  if (row_index == 0) {
+    // desperate
+    for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
+      POLY_BLOCK *pb = block_it.data()->block->pdblk.poly_block();
+      if (pb != nullptr && !pb->IsText()) {
+        continue; // Pretend non-text blocks don't exist.
+      }
+      TO_ROW_IT row_it(block_it.data()->get_rows());
+      for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+        row = row_it.data();
+        gradients[row_index] = row->line_m();
+        errors[row_index] = row->line_error();
+        row_index++;
+      }
+    }
+  }
+  row_count = row_index;
+  row_index = static_cast<int32_t>(row_count * textord_skew_ile);
+  gradients.resize(row_count);
+  std::nth_element(gradients.begin(), gradients.begin() + row_index, gradients.end());
+  page_m = gradients[row_index];
+  row_index = static_cast<int32_t>(row_count * textord_skew_ile);
+  errors.resize(row_count);
+  std::nth_element(errors.begin(), errors.begin() + row_index, errors.end());
+  page_err = errors[row_index];
+}
+
+const double kNoiseSize = 0.5; // Fraction of xheight.
+const int kMinSize = 8;        // Min pixels to be xheight.
+
+/**
+ * Return true if the dot looks like it is part of the i.
+ * Doesn't work for any other diacritical.
+ */
+static bool dot_of_i(BLOBNBOX *dot, BLOBNBOX *i, TO_ROW *row) {
+  const TBOX &ibox = i->bounding_box();
+  const TBOX &dotbox = dot->bounding_box();
+
+  // Must overlap horizontally by enough and be high enough.
+  int overlap = std::min(dotbox.right(), ibox.right()) - std::max(dotbox.left(), ibox.left());
+  if (ibox.height() <= 2 * dotbox.height() ||
+      (overlap * 2 < ibox.width() && overlap < dotbox.width())) {
+    return false;
+  }
+
+  // If the i is tall and thin then it is good.
+  if (ibox.height() > ibox.width() * 2) {
+    return true; // The i or ! must be tall and thin.
+  }
+
+  // It might still be tall and thin, but it might be joined to something.
+  // So search the outline for a piece of large height close to the edges
+  // of the dot.
+  const double kHeightFraction = 0.6;
+  double target_height = std::min(dotbox.bottom(), ibox.top());
+  target_height -= row->line_m() * dotbox.left() + row->line_c();
+  target_height *= kHeightFraction;
+  int left_min = dotbox.left() - dotbox.width();
+  int middle = (dotbox.left() + dotbox.right()) / 2;
+  int right_max = dotbox.right() + dotbox.width();
+  int left_miny = 0;
+  int left_maxy = 0;
+  int right_miny = 0;
+  int right_maxy = 0;
+  bool found_left = false;
+  bool found_right = false;
+  bool in_left = false;
+  bool in_right = false;
+  C_BLOB *blob = i->cblob();
+  C_OUTLINE_IT o_it = blob->out_list();
+  for (o_it.mark_cycle_pt(); !o_it.cycled_list(); o_it.forward()) {
+    C_OUTLINE *outline = o_it.data();
+    int length = outline->pathlength();
+    ICOORD pos = outline->start_pos();
+    for (int step = 0; step < length; pos += outline->step(step++)) {
+      int x = pos.x();
+      int y = pos.y();
+      if (x >= left_min && x < middle && !found_left) {
+        // We are in the left part so find min and max y.
+        if (in_left) {
+          if (y > left_maxy) {
+            left_maxy = y;
+          }
+          if (y < left_miny) {
+            left_miny = y;
+          }
+        } else {
+          left_maxy = left_miny = y;
+          in_left = true;
+        }
+      } else if (in_left) {
+        // We just left the left so look for size.
+        if (left_maxy - left_miny > target_height) {
+          if (found_right) {
+            return true;
+          }
+          found_left = true;
+        }
+        in_left = false;
+      }
+      if (x <= right_max && x > middle && !found_right) {
+        // We are in the right part so find min and max y.
+        if (in_right) {
+          if (y > right_maxy) {
+            right_maxy = y;
+          }
+          if (y < right_miny) {
+            right_miny = y;
+          }
+        } else {
+          right_maxy = right_miny = y;
+          in_right = true;
+        }
+      } else if (in_right) {
+        // We just left the right so look for size.
+        if (right_maxy - right_miny > target_height) {
+          if (found_left) {
+            return true;
+          }
+          found_right = true;
+        }
+        in_right = false;
+      }
+    }
+  }
+  return false;
+}
+
+void vigorous_noise_removal(TO_BLOCK *block) {
+  TO_ROW_IT row_it = block->get_rows();
+  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+    TO_ROW *row = row_it.data();
+    BLOBNBOX_IT b_it = row->blob_list();
+    // Estimate the xheight on the row.
+    int max_height = 0;
+    for (b_it.mark_cycle_pt(); !b_it.cycled_list(); b_it.forward()) {
+      BLOBNBOX *blob = b_it.data();
+      if (blob->bounding_box().height() > max_height) {
+        max_height = blob->bounding_box().height();
+      }
+    }
+    STATS hstats(0, max_height);
+    for (b_it.mark_cycle_pt(); !b_it.cycled_list(); b_it.forward()) {
+      BLOBNBOX *blob = b_it.data();
+      int height = blob->bounding_box().height();
+      if (height >= kMinSize) {
+        hstats.add(blob->bounding_box().height(), 1);
+      }
+    }
+    float xheight = hstats.median();
+    // Delete small objects.
+    BLOBNBOX *prev = nullptr;
+    for (b_it.mark_cycle_pt(); !b_it.cycled_list(); b_it.forward()) {
+      BLOBNBOX *blob = b_it.data();
+      const TBOX &box = blob->bounding_box();
+      if (box.height() < kNoiseSize * xheight) {
+        // Small so delete unless it looks like an i dot.
+        if (prev != nullptr) {
+          if (dot_of_i(blob, prev, row)) {
+            continue; // Looks OK.
+          }
+        }
+        if (!b_it.at_last()) {
+          BLOBNBOX *next = b_it.data_relative(1);
+          if (dot_of_i(blob, next, row)) {
+            continue; // Looks OK.
+          }
+        }
+        // It might be noise so get rid of it.
+        delete blob->remove_cblob();
+        delete b_it.extract();
+      } else {
+        prev = blob;
+      }
+    }
+  }
+}
+
+/**
+ * cleanup_rows_making
+ *
+ * Remove overlapping rows and fit all the blobs to what's left.
+ */
+void cleanup_rows_making( // find lines
+    ICOORD page_tr,       // top right
+    TO_BLOCK *block,      // block to do
+    float gradient,       // gradient to fit
+    FCOORD rotation,      // for drawing
+    int32_t block_edge,   // edge of block
+    bool testing_on       // correct orientation
+) {
+  // iterators
+  BLOBNBOX_IT blob_it = &block->blobs;
+  TO_ROW_IT row_it = block->get_rows();
+
+#ifndef GRAPHICS_DISABLED
+  if (textord_show_parallel_rows && testing_on) {
+    if (to_win == nullptr) {
+      create_to_win(page_tr);
+    }
+  }
+#endif
+  // get row coords
+  fit_parallel_rows(block, gradient, rotation, block_edge,
+                    textord_show_parallel_rows && testing_on);
+  delete_non_dropout_rows(block, gradient, rotation, block_edge,
+                          textord_show_parallel_rows && testing_on);
+  expand_rows(page_tr, block, gradient, rotation, block_edge, testing_on);
+  blob_it.set_to_list(&block->blobs);
+  row_it.set_to_list(block->get_rows());
+  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+    blob_it.add_list_after(row_it.data()->blob_list());
+  }
+  // give blobs back
+  assign_blobs_to_rows(block, &gradient, 1, false, false, false);
+  // now new rows must be genuine
+  blob_it.set_to_list(&block->blobs);
+  blob_it.add_list_after(&block->large_blobs);
+  assign_blobs_to_rows(block, &gradient, 2, true, true, false);
+  // safe to use big ones now
+  blob_it.set_to_list(&block->blobs);
+  // throw all blobs in
+  blob_it.add_list_after(&block->noise_blobs);
+  blob_it.add_list_after(&block->small_blobs);
+  assign_blobs_to_rows(block, &gradient, 3, false, false, false);
+}
+
+/**
+ * delete_non_dropout_rows
+ *
+ * Compute the linespacing and offset.
+ */
+void delete_non_dropout_rows( // find lines
+    TO_BLOCK *block,          // block to do
+    float gradient,           // global skew
+    FCOORD rotation,          // deskew vector
+    int32_t block_edge,       // left edge
+    bool testing_on           // correct orientation
+) {
+  TBOX block_box; // deskewed block
+  int32_t max_y;  // in block
+  int32_t min_y;
+  int32_t line_index; // of scan line
+  int32_t line_count; // no of scan lines
+  int32_t distance;   // to drop-out
+  int32_t xleft;      // of block
+  int32_t ybottom;    // of block
+  TO_ROW *row;        // current row
+  TO_ROW_IT row_it = block->get_rows();
+  BLOBNBOX_IT blob_it = &block->blobs;
+
+  if (row_it.empty()) {
+    return; // empty block
+  }
+  block_box = deskew_block_coords(block, gradient);
+  xleft = block->block->pdblk.bounding_box().left();
+  ybottom = block->block->pdblk.bounding_box().bottom();
+  min_y = block_box.bottom() - 1;
+  max_y = block_box.top() + 1;
+  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+    line_index = static_cast<int32_t>(std::floor(row_it.data()->intercept()));
+    if (line_index <= min_y) {
+      min_y = line_index - 1;
+    }
+    if (line_index >= max_y) {
+      max_y = line_index + 1;
+    }
+  }
+  line_count = max_y - min_y + 1;
+  if (line_count <= 0) {
+    return; // empty block
+  }
+  // change in occupation
+  std::vector<int32_t> deltas(line_count);
+  // of pixel coords
+  std::vector<int32_t> occupation(line_count);
+
+  compute_line_occupation(block, gradient, min_y, max_y, &occupation[0], &deltas[0]);
+  compute_occupation_threshold(
+      static_cast<int32_t>(ceil(block->line_spacing * (tesseract::CCStruct::kDescenderFraction +
+                                                       tesseract::CCStruct::kAscenderFraction))),
+      static_cast<int32_t>(ceil(block->line_spacing * (tesseract::CCStruct::kXHeightFraction +
+                                                       tesseract::CCStruct::kAscenderFraction))),
+      max_y - min_y + 1, &occupation[0], &deltas[0]);
+#ifndef GRAPHICS_DISABLED
+  if (testing_on) {
+    draw_occupation(xleft, ybottom, min_y, max_y, &occupation[0], &deltas[0]);
+  }
+#endif
+  compute_dropout_distances(&occupation[0], &deltas[0], line_count);
+  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+    row = row_it.data();
+    line_index = static_cast<int32_t>(std::floor(row->intercept()));
+    distance = deltas[line_index - min_y];
+    if (find_best_dropout_row(row, distance, block->line_spacing / 2, line_index, &row_it,
+                              testing_on)) {
+#ifndef GRAPHICS_DISABLED
+      if (testing_on) {
+        plot_parallel_row(row, gradient, block_edge, ScrollView::WHITE, rotation);
+      }
+#endif
+      blob_it.add_list_after(row_it.data()->blob_list());
+      delete row_it.extract(); // too far away
+    }
+  }
+  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+    blob_it.add_list_after(row_it.data()->blob_list());
+  }
+}
+
+/**
+ * @name find_best_dropout_row
+ *
+ * Delete this row if it has a neighbour with better dropout characteristics.
+ * true is returned if the row should be deleted.
+ */
+bool find_best_dropout_row( // find neighbours
+    TO_ROW *row,            // row to test
+    int32_t distance,       // dropout dist
+    float dist_limit,       // threshold distance
+    int32_t line_index,     // index of row
+    TO_ROW_IT *row_it,      // current position
+    bool testing_on         // correct orientation
+) {
+  int32_t next_index; // of neighbouring row
+  int32_t row_offset; // from current row
+  int32_t abs_dist;   // absolute distance
+  int8_t row_inc;     // increment to row_index
+  TO_ROW *next_row;   // nextious row
+
+  if (testing_on) {
+    tprintf("Row at %g(%g), dropout dist=%d,", row->intercept(), row->parallel_c(), distance);
+  }
+  if (distance < 0) {
+    row_inc = 1;
+    abs_dist = -distance;
+  } else {
+    row_inc = -1;
+    abs_dist = distance;
+  }
+  if (abs_dist > dist_limit) {
+    if (testing_on) {
+      tprintf(" too far - deleting\n");
+    }
+    return true;
+  }
+  if ((distance < 0 && !row_it->at_last()) || (distance >= 0 && !row_it->at_first())) {
+    row_offset = row_inc;
+    do {
+      next_row = row_it->data_relative(row_offset);
+      next_index = static_cast<int32_t>(std::floor(next_row->intercept()));
+      if ((distance < 0 && next_index < line_index &&
+           next_index > line_index + distance + distance) ||
+          (distance >= 0 && next_index > line_index &&
+           next_index < line_index + distance + distance)) {
+        if (testing_on) {
+          tprintf(" nearer neighbour (%d) at %g\n", line_index + distance - next_index,
+                  next_row->intercept());
+        }
+        return true; // other is nearer
+      } else if (next_index == line_index || next_index == line_index + distance + distance) {
+        if (row->believability() <= next_row->believability()) {
+          if (testing_on) {
+            tprintf(" equal but more believable at %g (%g/%g)\n", next_row->intercept(),
+                    row->believability(), next_row->believability());
+          }
+          return true; // other is more believable
+        }
+      }
+      row_offset += row_inc;
+    } while ((next_index == line_index || next_index == line_index + distance + distance) &&
+             row_offset < row_it->length());
+    if (testing_on) {
+      tprintf(" keeping\n");
+    }
+  }
+  return false;
+}
+
+/**
+ * @name deskew_block_coords
+ *
+ * Compute the bounding box of all the blobs in the block
+ * if they were deskewed without actually doing it.
+ */
+TBOX deskew_block_coords( // block box
+    TO_BLOCK *block,      // block to do
+    float gradient        // global skew
+) {
+  TBOX result;     // block bounds
+  TBOX blob_box;   // of block
+  FCOORD rotation; // deskew vector
+  float length;    // of gradient vector
+  TO_ROW_IT row_it = block->get_rows();
+  TO_ROW *row;         // current row
+  BLOBNBOX *blob;      // current blob
+  BLOBNBOX_IT blob_it; // iterator
+
+  length = std::sqrt(gradient * gradient + 1);
+  rotation = FCOORD(1 / length, -gradient / length);
+  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+    row = row_it.data();
+    blob_it.set_to_list(row->blob_list());
+    for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
+      blob = blob_it.data();
+      blob_box = blob->bounding_box();
+      blob_box.rotate(rotation); // de-skew it
+      result += blob_box;
+    }
+  }
+  return result;
+}
+
+/**
+ * @name compute_line_occupation
+ *
+ * Compute the pixel projection back on the y axis given the global
+ * skew. Also compute the 1st derivative.
+ */
+void compute_line_occupation( // project blobs
+    TO_BLOCK *block,          // block to do
+    float gradient,           // global skew
+    int32_t min_y,            // min coord in block
+    int32_t max_y,            // in block
+    int32_t *occupation,      // output projection
+    int32_t *deltas           // derivative
+) {
+  int32_t line_count; // maxy-miny+1
+  int32_t line_index; // of scan line
+  int index;          // array index for daft compilers
+  TO_ROW *row;        // current row
+  TO_ROW_IT row_it = block->get_rows();
+  BLOBNBOX *blob;      // current blob
+  BLOBNBOX_IT blob_it; // iterator
+  float length;        // of skew vector
+  TBOX blob_box;       // bounding box
+  FCOORD rotation;     // inverse of skew
+
+  line_count = max_y - min_y + 1;
+  length = std::sqrt(gradient * gradient + 1);
+  rotation = FCOORD(1 / length, -gradient / length);
+  for (line_index = 0; line_index < line_count; line_index++) {
+    deltas[line_index] = 0;
+  }
+  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+    row = row_it.data();
+    blob_it.set_to_list(row->blob_list());
+    for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
+      blob = blob_it.data();
+      blob_box = blob->bounding_box();
+      blob_box.rotate(rotation); // de-skew it
+      int32_t width = blob_box.right() - blob_box.left();
+      index = blob_box.bottom() - min_y;
+      ASSERT_HOST(index >= 0 && index < line_count);
+      // count transitions
+      deltas[index] += width;
+      index = blob_box.top() - min_y;
+      ASSERT_HOST(index >= 0 && index < line_count);
+      deltas[index] -= width;
+    }
+  }
+  occupation[0] = deltas[0];
+  for (line_index = 1; line_index < line_count; line_index++) {
+    occupation[line_index] = occupation[line_index - 1] + deltas[line_index];
+  }
+}
+
+/**
+ * compute_occupation_threshold
+ *
+ * Compute thresholds for textline or not for the occupation array.
+ */
+void compute_occupation_threshold( // project blobs
+    int32_t low_window,            // below result point
+    int32_t high_window,           // above result point
+    int32_t line_count,            // array sizes
+    int32_t *occupation,           // input projection
+    int32_t *thresholds            // output thresholds
+) {
+  int32_t line_index; // of thresholds line
+  int32_t low_index;  // in occupation
+  int32_t high_index; // in occupation
+  int32_t sum;        // current average
+  int32_t divisor;    // to get thresholds
+  int32_t min_index;  // of min occ
+  int32_t min_occ;    // min in locality
+  int32_t test_index; // for finding min
+
+  divisor = static_cast<int32_t>(ceil((low_window + high_window) / textord_occupancy_threshold));
+  if (low_window + high_window < line_count) {
+    for (sum = 0, high_index = 0; high_index < low_window; high_index++) {
+      sum += occupation[high_index];
+    }
+    for (low_index = 0; low_index < high_window; low_index++, high_index++) {
+      sum += occupation[high_index];
+    }
+    min_occ = occupation[0];
+    min_index = 0;
+    for (test_index = 1; test_index < high_index; test_index++) {
+      if (occupation[test_index] <= min_occ) {
+        min_occ = occupation[test_index];
+        min_index = test_index; // find min in region
+      }
+    }
+    for (line_index = 0; line_index < low_window; line_index++) {
+      thresholds[line_index] = (sum - min_occ) / divisor + min_occ;
+    }
+    // same out to end
+    for (low_index = 0; high_index < line_count; low_index++, high_index++) {
+      sum -= occupation[low_index];
+      sum += occupation[high_index];
+      if (occupation[high_index] <= min_occ) {
+        // find min in region
+        min_occ = occupation[high_index];
+        min_index = high_index;
+      }
+      // lost min from region
+      if (min_index <= low_index) {
+        min_occ = occupation[low_index + 1];
+        min_index = low_index + 1;
+        for (test_index = low_index + 2; test_index <= high_index; test_index++) {
+          if (occupation[test_index] <= min_occ) {
+            min_occ = occupation[test_index];
+            // find min in region
+            min_index = test_index;
+          }
+        }
+      }
+      thresholds[line_index++] = (sum - min_occ) / divisor + min_occ;
+    }
+  } else {
+    min_occ = occupation[0];
+    min_index = 0;
+    for (sum = 0, low_index = 0; low_index < line_count; low_index++) {
+      if (occupation[low_index] < min_occ) {
+        min_occ = occupation[low_index];
+        min_index = low_index;
+      }
+      sum += occupation[low_index];
+    }
+    line_index = 0;
+  }
+  for (; line_index < line_count; line_index++) {
+    thresholds[line_index] = (sum - min_occ) / divisor + min_occ;
+  }
+  // same out to end
+}
+
+/**
+ * @name compute_dropout_distances
+ *
+ * Compute the distance from each coordinate to the nearest dropout.
+ */
+void compute_dropout_distances( // project blobs
+    int32_t *occupation,        // input projection
+    int32_t *thresholds,        // output thresholds
+    int32_t line_count          // array sizes
+) {
+  int32_t line_index;     // of thresholds line
+  int32_t distance;       // from prev dropout
+  int32_t next_dist;      // to next dropout
+  int32_t back_index;     // for back filling
+  int32_t prev_threshold; // before overwrite
+
+  distance = -line_count;
+  line_index = 0;
+  do {
+    do {
+      distance--;
+      prev_threshold = thresholds[line_index];
+      // distance from prev
+      thresholds[line_index] = distance;
+      line_index++;
+    } while (line_index < line_count && (occupation[line_index] < thresholds[line_index] ||
+                                         occupation[line_index - 1] >= prev_threshold));
+    if (line_index < line_count) {
+      back_index = line_index - 1;
+      next_dist = 1;
+      while (next_dist < -distance && back_index >= 0) {
+        thresholds[back_index] = next_dist;
+        back_index--;
+        next_dist++;
+        distance++;
+      }
+      distance = 1;
+    }
+  } while (line_index < line_count);
+}
+
+/**
+ * @name expand_rows
+ *
+ * Expand each row to the least of its allowed size and touching its
+ * neighbours. If the expansion would entirely swallow a neighbouring row
+ * then do so.
+ */
+void expand_rows(       // find lines
+    ICOORD page_tr,     // top right
+    TO_BLOCK *block,    // block to do
+    float gradient,     // gradient to fit
+    FCOORD rotation,    // for drawing
+    int32_t block_edge, // edge of block
+    bool testing_on     // correct orientation
+) {
+  bool swallowed_row;    // eaten a neighbour
+  float y_max, y_min;    // new row limits
+  float y_bottom, y_top; // allowed limits
+  TO_ROW *test_row;      // next row
+  TO_ROW *row;           // current row
+                         // iterators
+  BLOBNBOX_IT blob_it = &block->blobs;
+  TO_ROW_IT row_it = block->get_rows();
+
+#ifndef GRAPHICS_DISABLED
+  if (textord_show_expanded_rows && testing_on) {
+    if (to_win == nullptr) {
+      create_to_win(page_tr);
+    }
+  }
+#endif
+
+  adjust_row_limits(block); // shift min,max.
+  if (textord_new_initial_xheight) {
+    if (block->get_rows()->empty()) {
+      return;
+    }
+    compute_row_stats(block, textord_show_expanded_rows && testing_on);
+  }
+  assign_blobs_to_rows(block, &gradient, 4, true, false, false);
+  // get real membership
+  if (block->get_rows()->empty()) {
+    return;
+  }
+  fit_parallel_rows(block, gradient, rotation, block_edge,
+                    textord_show_expanded_rows && testing_on);
+  if (!textord_new_initial_xheight) {
+    compute_row_stats(block, textord_show_expanded_rows && testing_on);
+  }
+  row_it.move_to_last();
+  do {
+    row = row_it.data();
+    y_max = row->max_y(); // get current limits
+    y_min = row->min_y();
+    y_bottom = row->intercept() - block->line_size * textord_expansion_factor *
+                                      tesseract::CCStruct::kDescenderFraction;
+    y_top = row->intercept() +
+            block->line_size * textord_expansion_factor *
+                (tesseract::CCStruct::kXHeightFraction + tesseract::CCStruct::kAscenderFraction);
+    if (y_min > y_bottom) { // expansion allowed
+      if (textord_show_expanded_rows && testing_on) {
+        tprintf("Expanding bottom of row at %f from %f to %f\n", row->intercept(), y_min, y_bottom);
+      }
+      // expandable
+      swallowed_row = true;
+      while (swallowed_row && !row_it.at_last()) {
+        swallowed_row = false;
+        // get next one
+        test_row = row_it.data_relative(1);
+        // overlaps space
+        if (test_row->max_y() > y_bottom) {
+          if (test_row->min_y() > y_bottom) {
+            if (textord_show_expanded_rows && testing_on) {
+              tprintf("Eating row below at %f\n", test_row->intercept());
+            }
+            row_it.forward();
+#ifndef GRAPHICS_DISABLED
+            if (textord_show_expanded_rows && testing_on) {
+              plot_parallel_row(test_row, gradient, block_edge, ScrollView::WHITE, rotation);
+            }
+#endif
+            blob_it.set_to_list(row->blob_list());
+            blob_it.add_list_after(test_row->blob_list());
+            // swallow complete row
+            delete row_it.extract();
+            row_it.backward();
+            swallowed_row = true;
+          } else if (test_row->max_y() < y_min) {
+            // shorter limit
+            y_bottom = test_row->max_y();
+            if (textord_show_expanded_rows && testing_on) {
+              tprintf("Truncating limit to %f due to touching row at %f\n", y_bottom,
+                      test_row->intercept());
+            }
+          } else {
+            y_bottom = y_min; // can't expand it
+            if (textord_show_expanded_rows && testing_on) {
+              tprintf("Not expanding limit beyond %f due to touching row at %f\n", y_bottom,
+                      test_row->intercept());
+            }
+          }
+        }
+      }
+      y_min = y_bottom; // expand it
+    }
+    if (y_max < y_top) { // expansion allowed
+      if (textord_show_expanded_rows && testing_on) {
+        tprintf("Expanding top of row at %f from %f to %f\n", row->intercept(), y_max, y_top);
+      }
+      swallowed_row = true;
+      while (swallowed_row && !row_it.at_first()) {
+        swallowed_row = false;
+        // get one above
+        test_row = row_it.data_relative(-1);
+        if (test_row->min_y() < y_top) {
+          if (test_row->max_y() < y_top) {
+            if (textord_show_expanded_rows && testing_on) {
+              tprintf("Eating row above at %f\n", test_row->intercept());
+            }
+            row_it.backward();
+            blob_it.set_to_list(row->blob_list());
+#ifndef GRAPHICS_DISABLED
+            if (textord_show_expanded_rows && testing_on) {
+              plot_parallel_row(test_row, gradient, block_edge, ScrollView::WHITE, rotation);
+            }
+#endif
+            blob_it.add_list_after(test_row->blob_list());
+            // swallow complete row
+            delete row_it.extract();
+            row_it.forward();
+            swallowed_row = true;
+          } else if (test_row->min_y() < y_max) {
+            // shorter limit
+            y_top = test_row->min_y();
+            if (textord_show_expanded_rows && testing_on) {
+              tprintf("Truncating limit to %f due to touching row at %f\n", y_top,
+                      test_row->intercept());
+            }
+          } else {
+            y_top = y_max; // can't expand it
+            if (textord_show_expanded_rows && testing_on) {
+              tprintf("Not expanding limit beyond %f due to touching row at %f\n", y_top,
+                      test_row->intercept());
+            }
+          }
+        }
+      }
+      y_max = y_top;
+    }
+    // new limits
+    row->set_limits(y_min, y_max);
+    row_it.backward();
+  } while (!row_it.at_last());
+}
+
+/**
+ * adjust_row_limits
+ *
+ * Change the limits of rows to suit the default fractions.
+ */
+void adjust_row_limits( // tidy limits
+    TO_BLOCK *block     // block to do
+) {
+  TO_ROW *row; // current row
+  float size;  // size of row
+  float ymax;  // top of row
+  float ymin;  // bottom of row
+  TO_ROW_IT row_it = block->get_rows();
+
+  if (textord_show_expanded_rows) {
+    tprintf("Adjusting row limits for block(%d,%d)\n", block->block->pdblk.bounding_box().left(),
+            block->block->pdblk.bounding_box().top());
+  }
+  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+    row = row_it.data();
+    size = row->max_y() - row->min_y();
+    if (textord_show_expanded_rows) {
+      tprintf("Row at %f has min %f, max %f, size %f\n", row->intercept(), row->min_y(),
+              row->max_y(), size);
+    }
+    size /= tesseract::CCStruct::kXHeightFraction + tesseract::CCStruct::kAscenderFraction +
+            tesseract::CCStruct::kDescenderFraction;
+    ymax = size * (tesseract::CCStruct::kXHeightFraction + tesseract::CCStruct::kAscenderFraction);
+    ymin = -size * tesseract::CCStruct::kDescenderFraction;
+    row->set_limits(row->intercept() + ymin, row->intercept() + ymax);
+    row->merged = false;
+  }
+}
+
+/**
+ * @name compute_row_stats
+ *
+ * Compute the linespacing and offset.
+ */
+void compute_row_stats( // find lines
+    TO_BLOCK *block,    // block to do
+    bool testing_on     // correct orientation
+) {
+  int32_t row_index; // of median
+  TO_ROW *row;       // current row
+  TO_ROW *prev_row;  // previous row
+  float iqr;         // inter quartile range
+  TO_ROW_IT row_it = block->get_rows();
+  // number of rows
+  int16_t rowcount = row_it.length();
+  // for choose nth
+  std::vector<TO_ROW *> rows(rowcount);
+  rowcount = 0;
+  prev_row = nullptr;
+  row_it.move_to_last(); // start at bottom
+  do {
+    row = row_it.data();
+    if (prev_row != nullptr) {
+      rows[rowcount++] = prev_row;
+      prev_row->spacing = row->intercept() - prev_row->intercept();
+      if (prev_row->spacing < 0.1 && prev_row->spacing > -0.1) {
+        // Avoid small spacing values which give a small disp_quant_factor_.
+        // That can cause large memory allocations with out-of-memory.
+        prev_row->spacing = 0;
+      }
+      if (testing_on) {
+        tprintf("Row at %g yields spacing of %g\n", row->intercept(), prev_row->spacing);
+      }
+    }
+    prev_row = row;
+    row_it.backward();
+  } while (!row_it.at_last());
+  block->key_row = prev_row;
+  block->baseline_offset = std::fmod(prev_row->parallel_c(), block->line_spacing);
+  if (testing_on) {
+    tprintf("Blob based spacing=(%g,%g), offset=%g", block->line_size, block->line_spacing,
+            block->baseline_offset);
+  }
+  if (rowcount > 0) {
+    rows.resize(rowcount);
+    row_index = rowcount * 3 / 4;
+    std::nth_element(rows.begin(), rows.begin() + row_index, rows.end(), row_spacing_order);
+    iqr = rows[row_index]->spacing;
+    row_index = rowcount / 4;
+    std::nth_element(rows.begin(), rows.begin() + row_index, rows.end(), row_spacing_order);
+    iqr -= rows[row_index]->spacing;
+    row_index = rowcount / 2;
+    std::nth_element(rows.begin(), rows.begin() + row_index, rows.end(), row_spacing_order);
+    block->key_row = rows[row_index];
+    if (testing_on) {
+      tprintf(" row based=%g(%g)", rows[row_index]->spacing, iqr);
+    }
+    if (rowcount > 2 && iqr < rows[row_index]->spacing * textord_linespace_iqrlimit) {
+      if (!textord_new_initial_xheight) {
+        if (rows[row_index]->spacing < block->line_spacing &&
+            rows[row_index]->spacing > block->line_size) {
+          // within range
+          block->line_size = rows[row_index]->spacing;
+        // spacing=size
+        } else if (rows[row_index]->spacing > block->line_spacing) {
+          block->line_size = block->line_spacing;
+        }
+        // too big so use max
+      } else {
+        if (rows[row_index]->spacing < block->line_spacing) {
+          block->line_size = rows[row_index]->spacing;
+        } else {
+          block->line_size = block->line_spacing;
+        }
+        // too big so use max
+      }
+      if (block->line_size < textord_min_xheight) {
+        block->line_size = (float)textord_min_xheight;
+      }
+      block->line_spacing = rows[row_index]->spacing;
+      block->max_blob_size = block->line_spacing * textord_excess_blobsize;
+    }
+    block->baseline_offset = std::fmod(rows[row_index]->intercept(), block->line_spacing);
+  }
+  if (testing_on) {
+    tprintf("\nEstimate line size=%g, spacing=%g, offset=%g\n", block->line_size,
+            block->line_spacing, block->baseline_offset);
+  }
+}
+
+/**
+ * @name compute_block_xheight
+ *
+ * Compute the xheight of the individual rows, then correlate them
+ * and interpret ascenderless lines, correcting xheights.
+ *
+ * First we compute our best guess of the x-height of each row independently
+ * with compute_row_xheight(), which looks for a pair of commonly occurring
+ * heights that could be x-height and ascender height. This function also
+ * attempts to find descenders of lowercase letters (i.e. not the small
+ * descenders that could appear in upper case letters as Q,J).
+ *
+ * After this computation each row falls into one of the following categories:
+ * ROW_ASCENDERS_FOUND: we found xheight and ascender modes, so this must be
+ *                      a regular row; we'll use its xheight to compute
+ *                      xheight and ascrise estimates for the block
+ * ROW_DESCENDERS_FOUND: no ascenders, so we do not have a high confidence in
+ *                       the xheight of this row (don't use it for estimating
+ *                       block xheight), but this row can't contain all caps
+ * ROW_UNKNOWN: a row with no ascenders/descenders, could be all lowercase
+ *              (or mostly lowercase for fonts with very few ascenders),
+ *              all upper case or small caps
+ * ROW_INVALID: no meaningful xheight could be found for this row
+ *
+ * We then run correct_row_xheight() and use the computed xheight and ascrise
+ * averages to correct xheight values of the rows in ROW_DESCENDERS_FOUND,
+ * ROW_UNKNOWN and ROW_INVALID categories.
+ *
+ */
+void Textord::compute_block_xheight(TO_BLOCK *block, float gradient) {
+  TO_ROW *row; // current row
+  float asc_frac_xheight = CCStruct::kAscenderFraction / CCStruct::kXHeightFraction;
+  float desc_frac_xheight = CCStruct::kDescenderFraction / CCStruct::kXHeightFraction;
+  int32_t min_height, max_height; // limits on xheight
+  TO_ROW_IT row_it = block->get_rows();
+  if (row_it.empty()) {
+    return; // no rows
+  }
+
+  // Compute the best guess of xheight of each row individually.
+  // Use xheight and ascrise values of the rows where ascenders were found.
+  get_min_max_xheight(block->line_size, &min_height, &max_height);
+  STATS row_asc_xheights(min_height, max_height);
+  STATS row_asc_ascrise(static_cast<int>(min_height * asc_frac_xheight),
+                        static_cast<int>(max_height * asc_frac_xheight));
+  int min_desc_height = static_cast<int>(min_height * desc_frac_xheight);
+  int max_desc_height = static_cast<int>(max_height * desc_frac_xheight);
+  STATS row_asc_descdrop(min_desc_height, max_desc_height);
+  STATS row_desc_xheights(min_height, max_height);
+  STATS row_desc_descdrop(min_desc_height, max_desc_height);
+  STATS row_cap_xheights(min_height, max_height);
+  STATS row_cap_floating_xheights(min_height, max_height);
+  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+    row = row_it.data();
+    // Compute the xheight of this row if it has not been computed before.
+    if (row->xheight <= 0) {
+      compute_row_xheight(row, block->block->classify_rotation(), gradient, block->line_size);
+    }
+    ROW_CATEGORY row_category = get_row_category(row);
+    if (row_category == ROW_ASCENDERS_FOUND) {
+      row_asc_xheights.add(static_cast<int32_t>(row->xheight), row->xheight_evidence);
+      row_asc_ascrise.add(static_cast<int32_t>(row->ascrise), row->xheight_evidence);
+      row_asc_descdrop.add(static_cast<int32_t>(-row->descdrop), row->xheight_evidence);
+    } else if (row_category == ROW_DESCENDERS_FOUND) {
+      row_desc_xheights.add(static_cast<int32_t>(row->xheight), row->xheight_evidence);
+      row_desc_descdrop.add(static_cast<int32_t>(-row->descdrop), row->xheight_evidence);
+    } else if (row_category == ROW_UNKNOWN) {
+      fill_heights(row, gradient, min_height, max_height, &row_cap_xheights,
+                   &row_cap_floating_xheights);
+    }
+  }
+
+  float xheight = 0.0;
+  float ascrise = 0.0;
+  float descdrop = 0.0;
+  // Compute our best guess of xheight of this block.
+  if (row_asc_xheights.get_total() > 0) {
+    // Determine xheight from rows where ascenders were found.
+    xheight = row_asc_xheights.median();
+    ascrise = row_asc_ascrise.median();
+    descdrop = -row_asc_descdrop.median();
+  } else if (row_desc_xheights.get_total() > 0) {
+    // Determine xheight from rows where descenders were found.
+    xheight = row_desc_xheights.median();
+    descdrop = -row_desc_descdrop.median();
+  } else if (row_cap_xheights.get_total() > 0) {
+    // All the rows in the block were (a/de)scenderless.
+    // Try to search for two modes in row_cap_heights that could
+    // be the xheight and the capheight (e.g. some of the rows
+    // were lowercase, but did not have enough (a/de)scenders.
+    // If such two modes cannot be found, this block is most
+    // likely all caps (or all small caps, in which case the code
+    // still works as intended).
+    compute_xheight_from_modes(
+        &row_cap_xheights, &row_cap_floating_xheights,
+        textord_single_height_mode && block->block->classify_rotation().y() == 0.0, min_height,
+        max_height, &(xheight), &(ascrise));
+    if (ascrise == 0) { // assume only caps in the whole block
+      xheight = row_cap_xheights.median() * CCStruct::kXHeightCapRatio;
+    }
+  } else { // default block sizes
+    xheight = block->line_size * CCStruct::kXHeightFraction;
+  }
+  // Correct xheight, ascrise and descdrop if necessary.
+  bool corrected_xheight = false;
+  if (xheight < textord_min_xheight) {
+    xheight = static_cast<float>(textord_min_xheight);
+    corrected_xheight = true;
+  }
+  if (corrected_xheight || ascrise <= 0) {
+    ascrise = xheight * asc_frac_xheight;
+  }
+  if (corrected_xheight || descdrop >= 0) {
+    descdrop = -(xheight * desc_frac_xheight);
+  }
+  block->xheight = xheight;
+
+  if (textord_debug_xheights) {
+    tprintf("Block average xheight=%.4f, ascrise=%.4f, descdrop=%.4f\n", xheight, ascrise,
+            descdrop);
+  }
+  // Correct xheight, ascrise, descdrop of rows based on block averages.
+  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+    correct_row_xheight(row_it.data(), xheight, ascrise, descdrop);
+  }
+}
+
+/**
+ * @name compute_row_xheight
+ *
+ * Estimate the xheight of this row.
+ * Compute the ascender rise and descender drop at the same time.
+ * Set xheigh_evidence to the number of blobs with the chosen xheight
+ * that appear in this row.
+ */
+void Textord::compute_row_xheight(TO_ROW *row, // row to do
+                                  const FCOORD &rotation,
+                                  float gradient, // global skew
+                                  int block_line_size) {
+  // Find blobs representing repeated characters in rows and mark them.
+  // This information is used for computing row xheight and at a later
+  // stage when words are formed by make_words.
+  if (!row->rep_chars_marked()) {
+    mark_repeated_chars(row);
+  }
+
+  int min_height, max_height;
+  get_min_max_xheight(block_line_size, &min_height, &max_height);
+  STATS heights(min_height, max_height);
+  STATS floating_heights(min_height, max_height);
+  fill_heights(row, gradient, min_height, max_height, &heights, &floating_heights);
+  row->ascrise = 0.0f;
+  row->xheight = 0.0f;
+  row->xheight_evidence = compute_xheight_from_modes(
+      &heights, &floating_heights, textord_single_height_mode && rotation.y() == 0.0, min_height,
+      max_height, &(row->xheight), &(row->ascrise));
+  row->descdrop = 0.0f;
+  if (row->xheight > 0) {
+    row->descdrop =
+        static_cast<float>(compute_row_descdrop(row, gradient, row->xheight_evidence, &heights));
+  }
+}
+
+/**
+ * @name fill_heights
+ *
+ * Fill the given heights with heights of the blobs that are legal
+ * candidates for estimating xheight.
+ */
+void fill_heights(TO_ROW *row, float gradient, int min_height, int max_height, STATS *heights,
+                  STATS *floating_heights) {
+  float xcentre;  // centre of blob
+  float top;      // top y coord of blob
+  float height;   // height of blob
+  BLOBNBOX *blob; // current blob
+  int repeated_set;
+  BLOBNBOX_IT blob_it = row->blob_list();
+  if (blob_it.empty()) {
+    return; // no blobs in this row
+  }
+  bool has_rep_chars = row->rep_chars_marked() && row->num_repeated_sets() > 0;
+  do {
+    blob = blob_it.data();
+    if (!blob->joined_to_prev()) {
+      xcentre = (blob->bounding_box().left() + blob->bounding_box().right()) / 2.0f;
+      top = blob->bounding_box().top();
+      height = blob->bounding_box().height();
+      if (textord_fix_xheight_bug) {
+        top -= row->baseline.y(xcentre);
+      } else {
+        top -= gradient * xcentre + row->parallel_c();
+      }
+      if (top >= min_height && top <= max_height) {
+        heights->add(static_cast<int32_t>(floor(top + 0.5)), 1);
+        if (height / top < textord_min_blob_height_fraction) {
+          floating_heights->add(static_cast<int32_t>(floor(top + 0.5)), 1);
+        }
+      }
+    }
+    // Skip repeated chars, since they are likely to skew the height stats.
+    if (has_rep_chars && blob->repeated_set() != 0) {
+      repeated_set = blob->repeated_set();
+      blob_it.forward();
+      while (!blob_it.at_first() && blob_it.data()->repeated_set() == repeated_set) {
+        blob_it.forward();
+        if (textord_debug_xheights) {
+          tprintf("Skipping repeated char when computing xheight\n");
+        }
+      }
+    } else {
+      blob_it.forward();
+    }
+  } while (!blob_it.at_first());
+}
+
+/**
+ * @name compute_xheight_from_modes
+ *
+ * Given a STATS object heights, looks for two most frequently occurring
+ * heights that look like xheight and xheight + ascrise. If found, sets
+ * the values of *xheight and *ascrise accordingly, otherwise sets xheight
+ * to any most frequently occurring height and sets *ascrise to 0.
+ * Returns the number of times xheight occurred in heights.
+ * For each mode that is considered for being an xheight the count of
+ * floating blobs (stored in floating_heights) is subtracted from the
+ * total count of the blobs of this height. This is done because blobs
+ * that sit far above the baseline could represent valid ascenders, but
+ * it is highly unlikely that such a character's height will be an xheight
+ * (e.g.  -, ', =, ^, `, ", ', etc)
+ * If cap_only, then force finding of only the top mode.
+ */
+int compute_xheight_from_modes(STATS *heights, STATS *floating_heights, bool cap_only,
+                               int min_height, int max_height, float *xheight, float *ascrise) {
+  int blob_index = heights->mode();                 // find mode
+  int blob_count = heights->pile_count(blob_index); // get count of mode
+  if (textord_debug_xheights) {
+    tprintf("min_height=%d, max_height=%d, mode=%d, count=%d, total=%d\n", min_height, max_height,
+            blob_index, blob_count, heights->get_total());
+    heights->print();
+    floating_heights->print();
+  }
+  if (blob_count == 0) {
+    return 0;
+  }
+  int modes[MAX_HEIGHT_MODES]; // biggest piles
+  bool in_best_pile = false;
+  int prev_size = -INT32_MAX;
+  int best_count = 0;
+  int mode_count = compute_height_modes(heights, min_height, max_height, modes, MAX_HEIGHT_MODES);
+  if (cap_only && mode_count > 1) {
+    mode_count = 1;
+  }
+  int x;
+  if (textord_debug_xheights) {
+    tprintf("found %d modes: ", mode_count);
+    for (x = 0; x < mode_count; x++) {
+      tprintf("%d ", modes[x]);
+    }
+    tprintf("\n");
+  }
+
+  for (x = 0; x < mode_count - 1; x++) {
+    if (modes[x] != prev_size + 1) {
+      in_best_pile = false; // had empty height
+    }
+    int modes_x_count = heights->pile_count(modes[x]) - floating_heights->pile_count(modes[x]);
+    if ((modes_x_count >= blob_count * textord_xheight_mode_fraction) &&
+        (in_best_pile || modes_x_count > best_count)) {
+      for (int asc = x + 1; asc < mode_count; asc++) {
+        float ratio = static_cast<float>(modes[asc]) / static_cast<float>(modes[x]);
+        if (textord_ascx_ratio_min < ratio && ratio < textord_ascx_ratio_max &&
+            (heights->pile_count(modes[asc]) >= blob_count * textord_ascheight_mode_fraction)) {
+          if (modes_x_count > best_count) {
+            in_best_pile = true;
+            best_count = modes_x_count;
+          }
+          if (textord_debug_xheights) {
+            tprintf("X=%d, asc=%d, count=%d, ratio=%g\n", modes[x], modes[asc] - modes[x],
+                    modes_x_count, ratio);
+          }
+          prev_size = modes[x];
+          *xheight = static_cast<float>(modes[x]);
+          *ascrise = static_cast<float>(modes[asc] - modes[x]);
+        }
+      }
+    }
+  }
+  if (*xheight == 0) { // single mode
+    // Remove counts of the "floating" blobs (the one whose height is too
+    // small in relation to it's top end of the bounding box) from heights
+    // before computing the single-mode xheight.
+    // Restore the counts in heights after the mode is found, since
+    // floating blobs might be useful for determining potential ascenders
+    // in compute_row_descdrop().
+    if (floating_heights->get_total() > 0) {
+      for (x = min_height; x < max_height; ++x) {
+        heights->add(x, -(floating_heights->pile_count(x)));
+      }
+      blob_index = heights->mode(); // find the modified mode
+      for (x = min_height; x < max_height; ++x) {
+        heights->add(x, floating_heights->pile_count(x));
+      }
+    }
+    *xheight = static_cast<float>(blob_index);
+    *ascrise = 0.0f;
+    best_count = heights->pile_count(blob_index);
+    if (textord_debug_xheights) {
+      tprintf("Single mode xheight set to %g\n", *xheight);
+    }
+  } else if (textord_debug_xheights) {
+    tprintf("Multi-mode xheight set to %g, asc=%g\n", *xheight, *ascrise);
+  }
+  return best_count;
+}
+
+/**
+ * @name compute_row_descdrop
+ *
+ * Estimates the descdrop of this row. This function looks for
+ * "significant" descenders of lowercase letters (those that could
+ * not just be the small descenders of upper case letters like Q,J).
+ * The function also takes into account how many potential ascenders
+ * this row might contain. If the number of potential ascenders along
+ * with descenders is close to the expected fraction of the total
+ * number of blobs in the row, the function returns the descender
+ * height, returns 0 otherwise.
+ */
+int32_t compute_row_descdrop(TO_ROW *row, float gradient, int xheight_blob_count,
+                             STATS *asc_heights) {
+  // Count how many potential ascenders are in this row.
+  int i_min = asc_heights->min_bucket();
+  if ((i_min / row->xheight) < textord_ascx_ratio_min) {
+    i_min = static_cast<int>(floor(row->xheight * textord_ascx_ratio_min + 0.5));
+  }
+  int i_max = asc_heights->max_bucket();
+  if ((i_max / row->xheight) > textord_ascx_ratio_max) {
+    i_max = static_cast<int>(floor(row->xheight * textord_ascx_ratio_max));
+  }
+  int num_potential_asc = 0;
+  for (int i = i_min; i <= i_max; ++i) {
+    num_potential_asc += asc_heights->pile_count(i);
+  }
+  auto min_height = static_cast<int32_t>(floor(row->xheight * textord_descx_ratio_min + 0.5));
+  auto max_height = static_cast<int32_t>(floor(row->xheight * textord_descx_ratio_max));
+  float xcentre; // centre of blob
+  float height;  // height of blob
+  BLOBNBOX_IT blob_it = row->blob_list();
+  BLOBNBOX *blob; // current blob
+  STATS heights(min_height, max_height);
+  for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
+    blob = blob_it.data();
+    if (!blob->joined_to_prev()) {
+      xcentre = (blob->bounding_box().left() + blob->bounding_box().right()) / 2.0f;
+      height = (gradient * xcentre + row->parallel_c() - blob->bounding_box().bottom());
+      if (height >= min_height && height <= max_height) {
+        heights.add(static_cast<int>(floor(height + 0.5)), 1);
+      }
+    }
+  }
+  int blob_index = heights.mode();                 // find mode
+  int blob_count = heights.pile_count(blob_index); // get count of mode
+  float total_fraction = (textord_descheight_mode_fraction + textord_ascheight_mode_fraction);
+  if (static_cast<float>(blob_count + num_potential_asc) < xheight_blob_count * total_fraction) {
+    blob_count = 0;
+  }
+  int descdrop = blob_count > 0 ? -blob_index : 0;
+  if (textord_debug_xheights) {
+    tprintf("Descdrop: %d (potential ascenders %d, descenders %d)\n", descdrop, num_potential_asc,
+            blob_count);
+    heights.print();
+  }
+  return descdrop;
+}
+
+/**
+ * @name compute_height_modes
+ *
+ * Find the top maxmodes values in the input array and put their
+ * indices in the output in the order in which they occurred.
+ */
+int32_t compute_height_modes(STATS *heights,     // stats to search
+                             int32_t min_height, // bottom of range
+                             int32_t max_height, // top of range
+                             int32_t *modes,     // output array
+                             int32_t maxmodes) { // size of modes
+  int32_t pile_count;                            // no in source pile
+  int32_t src_count;                             // no of source entries
+  int32_t src_index;                             // current entry
+  int32_t least_count;                           // height of smalllest
+  int32_t least_index;                           // index of least
+  int32_t dest_count;                            // index in modes
+
+  src_count = max_height + 1 - min_height;
+  dest_count = 0;
+  least_count = INT32_MAX;
+  least_index = -1;
+  for (src_index = 0; src_index < src_count; src_index++) {
+    pile_count = heights->pile_count(min_height + src_index);
+    if (pile_count > 0) {
+      if (dest_count < maxmodes) {
+        if (pile_count < least_count) {
+          // find smallest in array
+          least_count = pile_count;
+          least_index = dest_count;
+        }
+        modes[dest_count++] = min_height + src_index;
+      } else if (pile_count >= least_count) {
+        while (least_index < maxmodes - 1) {
+          modes[least_index] = modes[least_index + 1];
+          // shuffle up
+          least_index++;
+        }
+        // new one on end
+        modes[maxmodes - 1] = min_height + src_index;
+        if (pile_count == least_count) {
+          // new smallest
+          least_index = maxmodes - 1;
+        } else {
+          least_count = heights->pile_count(modes[0]);
+          least_index = 0;
+          for (dest_count = 1; dest_count < maxmodes; dest_count++) {
+            pile_count = heights->pile_count(modes[dest_count]);
+            if (pile_count < least_count) {
+              // find smallest
+              least_count = pile_count;
+              least_index = dest_count;
+            }
+          }
+        }
+      }
+    }
+  }
+  return dest_count;
+}
+
+/**
+ * @name correct_row_xheight
+ *
+ * Adjust the xheight etc of this row if not within reasonable limits
+ * of the average for the block.
+ */
+void correct_row_xheight(TO_ROW *row, float xheight, float ascrise, float descdrop) {
+  ROW_CATEGORY row_category = get_row_category(row);
+  if (textord_debug_xheights) {
+    tprintf(
+        "correcting row xheight: row->xheight %.4f"
+        ", row->acrise %.4f row->descdrop %.4f\n",
+        row->xheight, row->ascrise, row->descdrop);
+  }
+  bool normal_xheight = within_error_margin(row->xheight, xheight, textord_xheight_error_margin);
+  bool cap_xheight =
+      within_error_margin(row->xheight, xheight + ascrise, textord_xheight_error_margin);
+  // Use the average xheight/ascrise for the following cases:
+  // -- the xheight of the row could not be determined at all
+  // -- the row has descenders (e.g. "many groups", "ISBN 12345 p.3")
+  //    and its xheight is close to either cap height or average xheight
+  // -- the row does not have ascenders or descenders, but its xheight
+  //    is close to the average block xheight (e.g. row with "www.mmm.com")
+  if (row_category == ROW_ASCENDERS_FOUND) {
+    if (row->descdrop >= 0) {
+      row->descdrop = row->xheight * (descdrop / xheight);
+    }
+  } else if (row_category == ROW_INVALID ||
+             (row_category == ROW_DESCENDERS_FOUND && (normal_xheight || cap_xheight)) ||
+             (row_category == ROW_UNKNOWN && normal_xheight)) {
+    if (textord_debug_xheights) {
+      tprintf("using average xheight\n");
+    }
+    row->xheight = xheight;
+    row->ascrise = ascrise;
+    row->descdrop = descdrop;
+  } else if (row_category == ROW_DESCENDERS_FOUND) {
+    // Assume this is a row with mostly lowercase letters and it's xheight
+    // is computed correctly (unfortunately there is no way to distinguish
+    // this from the case when descenders are found, but the most common
+    // height is capheight).
+    if (textord_debug_xheights) {
+      tprintf("lowercase, corrected ascrise\n");
+    }
+    row->ascrise = row->xheight * (ascrise / xheight);
+  } else if (row_category == ROW_UNKNOWN) {
+    // Otherwise assume this row is an all-caps or small-caps row
+    // and adjust xheight and ascrise of the row.
+
+    row->all_caps = true;
+    if (cap_xheight) { // regular all caps
+      if (textord_debug_xheights) {
+        tprintf("all caps\n");
+      }
+      row->xheight = xheight;
+      row->ascrise = ascrise;
+      row->descdrop = descdrop;
+    } else { // small caps or caps with an odd xheight
+      if (textord_debug_xheights) {
+        if (row->xheight < xheight + ascrise && row->xheight > xheight) {
+          tprintf("small caps\n");
+        } else {
+          tprintf("all caps with irregular xheight\n");
+        }
+      }
+      row->ascrise = row->xheight * (ascrise / (xheight + ascrise));
+      row->xheight -= row->ascrise;
+      row->descdrop = row->xheight * (descdrop / xheight);
+    }
+  }
+  if (textord_debug_xheights) {
+    tprintf(
+        "corrected row->xheight = %.4f, row->acrise = %.4f, row->descdrop"
+        " = %.4f\n",
+        row->xheight, row->ascrise, row->descdrop);
+  }
+}
+
+static int CountOverlaps(const TBOX &box, int min_height, BLOBNBOX_LIST *blobs) {
+  int overlaps = 0;
+  BLOBNBOX_IT blob_it(blobs);
+  for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
+    BLOBNBOX *blob = blob_it.data();
+    const TBOX &blob_box = blob->bounding_box();
+    if (blob_box.height() >= min_height && box.major_overlap(blob_box)) {
+      ++overlaps;
+    }
+  }
+  return overlaps;
+}
+
+/**
+ * @name separate_underlines
+ *
+ * Test wide objects for being potential underlines. If they are then
+ * put them in a separate list in the block.
+ */
+void separate_underlines(TO_BLOCK *block,   // block to do
+                         float gradient,    // skew angle
+                         FCOORD rotation,   // inverse landscape
+                         bool testing_on) { // correct orientation
+  BLOBNBOX *blob;                           // current blob
+  C_BLOB *rotated_blob;                     // rotated blob
+  TO_ROW *row;                              // current row
+  float length;                             // of g_vec
+  TBOX blob_box;
+  FCOORD blob_rotation; // inverse of rotation
+  FCOORD g_vec;         // skew rotation
+  BLOBNBOX_IT blob_it;  // iterator
+                        // iterator
+  BLOBNBOX_IT under_it = &block->underlines;
+  BLOBNBOX_IT large_it = &block->large_blobs;
+  TO_ROW_IT row_it = block->get_rows();
+  int min_blob_height = static_cast<int>(textord_min_blob_height_fraction * block->line_size + 0.5);
+
+  // length of vector
+  length = std::sqrt(1 + gradient * gradient);
+  g_vec = FCOORD(1 / length, -gradient / length);
+  blob_rotation = FCOORD(rotation.x(), -rotation.y());
+  blob_rotation.rotate(g_vec); // undoing everything
+  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+    row = row_it.data();
+    // get blobs
+    blob_it.set_to_list(row->blob_list());
+    for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
+      blob = blob_it.data();
+      blob_box = blob->bounding_box();
+      if (blob_box.width() > block->line_size * textord_underline_width) {
+        ASSERT_HOST(blob->cblob() != nullptr);
+        rotated_blob = crotate_cblob(blob->cblob(), blob_rotation);
+        if (test_underline(testing_on && textord_show_final_rows, rotated_blob,
+                           static_cast<int16_t>(row->intercept()),
+                           static_cast<int16_t>(block->line_size *
+                                                (tesseract::CCStruct::kXHeightFraction +
+                                                 tesseract::CCStruct::kAscenderFraction / 2.0f)))) {
+          under_it.add_after_then_move(blob_it.extract());
+          if (testing_on && textord_show_final_rows) {
+            tprintf("Underlined blob at:");
+            rotated_blob->bounding_box().print();
+            tprintf("Was:");
+            blob_box.print();
+          }
+        } else if (CountOverlaps(blob->bounding_box(), min_blob_height, row->blob_list()) >
+                   textord_max_blob_overlaps) {
+          large_it.add_after_then_move(blob_it.extract());
+          if (testing_on && textord_show_final_rows) {
+            tprintf("Large blob overlaps %d blobs at:",
+                    CountOverlaps(blob_box, min_blob_height, row->blob_list()));
+            blob_box.print();
+          }
+        }
+        delete rotated_blob;
+      }
+    }
+  }
+}
+
+/**
+ * @name pre_associate_blobs
+ *
+ * Associate overlapping blobs and fake chop wide blobs.
+ */
+void pre_associate_blobs( // make rough chars
+    ICOORD page_tr,       // top right
+    TO_BLOCK *block,      // block to do
+    FCOORD rotation,      // inverse landscape
+    bool testing_on       // correct orientation
+) {
+#ifndef GRAPHICS_DISABLED
+  ScrollView::Color colour; // of boxes
+#endif
+  BLOBNBOX *blob;     // current blob
+  BLOBNBOX *nextblob; // next in list
+  TBOX blob_box;
+  FCOORD blob_rotation; // inverse of rotation
+  BLOBNBOX_IT blob_it;  // iterator
+  BLOBNBOX_IT start_it; // iterator
+  TO_ROW_IT row_it = block->get_rows();
+
+#ifndef GRAPHICS_DISABLED
+  colour = ScrollView::RED;
+#endif
+
+  blob_rotation = FCOORD(rotation.x(), -rotation.y());
+  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+    // get blobs
+    blob_it.set_to_list(row_it.data()->blob_list());
+    for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
+      blob = blob_it.data();
+      blob_box = blob->bounding_box();
+      start_it = blob_it; // save start point
+      //                      if (testing_on && textord_show_final_blobs)
+      //                      {
+      //                              tprintf("Blob at (%d,%d)->(%d,%d),
+      //                              addr=%x, count=%d\n",
+      //                                      blob_box.left(),blob_box.bottom(),
+      //                                      blob_box.right(),blob_box.top(),
+      //                                      (void*)blob,blob_it.length());
+      //                      }
+      bool overlap;
+      do {
+        overlap = false;
+        if (!blob_it.at_last()) {
+          nextblob = blob_it.data_relative(1);
+          overlap = blob_box.major_x_overlap(nextblob->bounding_box());
+          if (overlap) {
+            blob->merge(nextblob);           // merge new blob
+            blob_box = blob->bounding_box(); // get bigger box
+            blob_it.forward();
+          }
+        }
+      } while (overlap);
+      blob->chop(&start_it, &blob_it, blob_rotation,
+                 block->line_size * tesseract::CCStruct::kXHeightFraction * textord_chop_width);
+      // attempt chop
+    }
+#ifndef GRAPHICS_DISABLED
+    if (testing_on && textord_show_final_blobs) {
+      if (to_win == nullptr) {
+        create_to_win(page_tr);
+      }
+      to_win->Pen(colour);
+      for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
+        blob = blob_it.data();
+        blob_box = blob->bounding_box();
+        blob_box.rotate(rotation);
+        if (!blob->joined_to_prev()) {
+          to_win->Rectangle(blob_box.left(), blob_box.bottom(), blob_box.right(), blob_box.top());
+        }
+      }
+      colour = static_cast<ScrollView::Color>(colour + 1);
+      if (colour > ScrollView::MAGENTA) {
+        colour = ScrollView::RED;
+      }
+    }
+#endif
+  }
+}
+
+/**
+ * @name fit_parallel_rows
+ *
+ * Re-fit the rows in the block to the given gradient.
+ */
+void fit_parallel_rows( // find lines
+    TO_BLOCK *block,    // block to do
+    float gradient,     // gradient to fit
+    FCOORD rotation,    // for drawing
+    int32_t block_edge, // edge of block
+    bool testing_on     // correct orientation
+) {
+#ifndef GRAPHICS_DISABLED
+  ScrollView::Color colour; // of row
+#endif
+  TO_ROW_IT row_it = block->get_rows();
+
+  row_it.move_to_first();
+  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+    if (row_it.data()->blob_list()->empty()) {
+      delete row_it.extract(); // nothing in it
+    } else {
+      fit_parallel_lms(gradient, row_it.data());
+    }
+  }
+#ifndef GRAPHICS_DISABLED
+  if (testing_on) {
+    colour = ScrollView::RED;
+    for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+      plot_parallel_row(row_it.data(), gradient, block_edge, colour, rotation);
+      colour = static_cast<ScrollView::Color>(colour + 1);
+      if (colour > ScrollView::MAGENTA) {
+        colour = ScrollView::RED;
+      }
+    }
+  }
+#endif
+  row_it.sort(row_y_order); // may have gone out of order
+}
+
+/**
+ * @name fit_parallel_lms
+ *
+ * Fit an LMS line to a row.
+ * Make the fit parallel to the given gradient and set the
+ * row accordingly.
+ */
+void fit_parallel_lms(float gradient, TO_ROW *row) {
+  float c;       // fitted line
+  int blobcount; // no of blobs
+  tesseract::DetLineFit lms;
+  BLOBNBOX_IT blob_it = row->blob_list();
+
+  blobcount = 0;
+  for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
+    if (!blob_it.data()->joined_to_prev()) {
+      const TBOX &box = blob_it.data()->bounding_box();
+      lms.Add(ICOORD((box.left() + box.right()) / 2, box.bottom()));
+      blobcount++;
+    }
+  }
+  double error = lms.ConstrainedFit(gradient, &c);
+  row->set_parallel_line(gradient, c, error);
+  if (textord_straight_baselines && blobcount > textord_lms_line_trials) {
+    error = lms.Fit(&gradient, &c);
+  }
+  // set the other too
+  row->set_line(gradient, c, error);
+}
+
+/**
+ * @name make_spline_rows
+ *
+ * Re-fit the rows in the block to the given gradient.
+ */
+void Textord::make_spline_rows(TO_BLOCK *block, // block to do
+                               float gradient,  // gradient to fit
+                               bool testing_on) {
+#ifndef GRAPHICS_DISABLED
+  ScrollView::Color colour; // of row
+  if (testing_on && to_win == nullptr) {
+    create_to_win(page_tr_);
+  }
+#endif
+  TO_ROW_IT row_it = block->get_rows();
+
+  row_it.move_to_first();
+  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+    if (row_it.data()->blob_list()->empty()) {
+      delete row_it.extract(); // nothing in it
+    } else {
+      make_baseline_spline(row_it.data(), block);
+    }
+  }
+  if (textord_old_baselines) {
+#ifndef GRAPHICS_DISABLED
+    if (testing_on) {
+      colour = ScrollView::RED;
+      for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+        row_it.data()->baseline.plot(to_win, colour);
+        colour = static_cast<ScrollView::Color>(colour + 1);
+        if (colour > ScrollView::MAGENTA) {
+          colour = ScrollView::RED;
+        }
+      }
+    }
+#endif
+    make_old_baselines(block, testing_on, gradient);
+  }
+#ifndef GRAPHICS_DISABLED
+  if (testing_on) {
+    colour = ScrollView::RED;
+    for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+      row_it.data()->baseline.plot(to_win, colour);
+      colour = static_cast<ScrollView::Color>(colour + 1);
+      if (colour > ScrollView::MAGENTA) {
+        colour = ScrollView::RED;
+      }
+    }
+  }
+#endif
+}
+
+/**
+ * @name make_baseline_spline
+ *
+ * Fit an LMS line to a row.
+ * Make the fit parallel to the given gradient and set the
+ * row accordingly.
+ */
+void make_baseline_spline(TO_ROW *row, // row to fit
+                          TO_BLOCK *block) {
+  double *coeffs;   // quadratic coeffs
+  int32_t segments; // no of segments
+
+  // spline boundaries
+  auto *xstarts = new int32_t[row->blob_list()->length() + 1];
+  if (segment_baseline(row, block, segments, xstarts) && !textord_straight_baselines &&
+      !textord_parallel_baselines) {
+    coeffs = linear_spline_baseline(row, block, segments, xstarts);
+  } else {
+    xstarts[1] = xstarts[segments];
+    segments = 1;
+    coeffs = new double[3];
+    coeffs[0] = 0;
+    coeffs[1] = row->line_m();
+    coeffs[2] = row->line_c();
+  }
+  row->baseline = QSPLINE(segments, xstarts, coeffs);
+  delete[] coeffs;
+  delete[] xstarts;
+}
+
+/**
+ * @name segment_baseline
+ *
+ * Divide the baseline up into segments which require a different
+ * quadratic fitted to them.
+ * Return true if enough blobs were far enough away to need a quadratic.
+ */
+bool segment_baseline( // split baseline
+    TO_ROW *row,       // row to fit
+    TO_BLOCK *block,   // block it came from
+    int32_t &segments, // no fo segments
+    int32_t *xstarts   // coords of segments
+) {
+  bool needs_curve; // needs curved line
+  int blobcount;    // no of blobs
+  int blobindex;    // current blob
+  int last_state;   // above, on , below
+  int state;        // of current blob
+  float yshift;     // from baseline
+  TBOX box;         // blob box
+  TBOX new_box;     // new_it box
+  float middle;     // xcentre of blob
+                    // blobs
+  BLOBNBOX_IT blob_it = row->blob_list();
+  BLOBNBOX_IT new_it = blob_it; // front end
+  SORTED_FLOATS yshifts;        // shifts from baseline
+
+  needs_curve = false;
+  box = box_next_pre_chopped(&blob_it);
+  xstarts[0] = box.left();
+  segments = 1;
+  blobcount = row->blob_list()->length();
+  if (textord_oldbl_debug) {
+    tprintf("Segmenting baseline of %d blobs at (%d,%d)\n", blobcount, box.left(), box.bottom());
+  }
+  if (blobcount <= textord_spline_medianwin || blobcount < textord_spline_minblobs) {
+    blob_it.move_to_last();
+    box = blob_it.data()->bounding_box();
+    xstarts[1] = box.right();
+    return false;
+  }
+  last_state = 0;
+  new_it.mark_cycle_pt();
+  for (blobindex = 0; blobindex < textord_spline_medianwin; blobindex++) {
+    new_box = box_next_pre_chopped(&new_it);
+    middle = (new_box.left() + new_box.right()) / 2.0;
+    yshift = new_box.bottom() - row->line_m() * middle - row->line_c();
+    // record shift
+    yshifts.add(yshift, blobindex);
+    if (new_it.cycled_list()) {
+      xstarts[1] = new_box.right();
+      return false;
+    }
+  }
+  for (blobcount = 0; blobcount < textord_spline_medianwin / 2; blobcount++) {
+    box = box_next_pre_chopped(&blob_it);
+  }
+  do {
+    new_box = box_next_pre_chopped(&new_it);
+    // get middle one
+    yshift = yshifts[textord_spline_medianwin / 2];
+    if (yshift > textord_spline_shift_fraction * block->line_size) {
+      state = 1;
+    } else if (-yshift > textord_spline_shift_fraction * block->line_size) {
+      state = -1;
+    } else {
+      state = 0;
+    }
+    if (state != 0) {
+      needs_curve = true;
+    }
+    //              tprintf("State=%d, prev=%d, shift=%g\n",
+    //                      state,last_state,yshift);
+    if (state != last_state && blobcount > textord_spline_minblobs) {
+      xstarts[segments++] = box.left();
+      blobcount = 0;
+    }
+    last_state = state;
+    yshifts.remove(blobindex - textord_spline_medianwin);
+    box = box_next_pre_chopped(&blob_it);
+    middle = (new_box.left() + new_box.right()) / 2.0;
+    yshift = new_box.bottom() - row->line_m() * middle - row->line_c();
+    yshifts.add(yshift, blobindex);
+    blobindex++;
+    blobcount++;
+  } while (!new_it.cycled_list());
+  if (blobcount > textord_spline_minblobs || segments == 1) {
+    xstarts[segments] = new_box.right();
+  } else {
+    xstarts[--segments] = new_box.right();
+  }
+  if (textord_oldbl_debug) {
+    tprintf("Made %d segments on row at (%d,%d)\n", segments, box.right(), box.bottom());
+  }
+  return needs_curve;
+}
+
+/**
+ * @name linear_spline_baseline
+ *
+ * Divide the baseline up into segments which require a different
+ * quadratic fitted to them.
+ * @return true if enough blobs were far enough away to need a quadratic.
+ */
+double *linear_spline_baseline( // split baseline
+    TO_ROW *row,                // row to fit
+    TO_BLOCK *block,            // block it came from
+    int32_t &segments,          // no fo segments
+    int32_t xstarts[]           // coords of segments
+) {
+  int blobcount;         // no of blobs
+  int blobindex;         // current blob
+  int index1, index2;    // blob numbers
+  int blobs_per_segment; // blobs in each
+  TBOX box;              // blob box
+  TBOX new_box;          // new_it box
+                         // blobs
+  BLOBNBOX_IT blob_it = row->blob_list();
+  BLOBNBOX_IT new_it = blob_it; // front end
+  float b, c;                   // fitted curve
+  tesseract::DetLineFit lms;
+  int32_t segment; // current segment
+
+  box = box_next_pre_chopped(&blob_it);
+  xstarts[0] = box.left();
+  blobcount = 1;
+  while (!blob_it.at_first()) {
+    blobcount++;
+    box = box_next_pre_chopped(&blob_it);
+  }
+  segments = blobcount / textord_spline_medianwin;
+  if (segments < 1) {
+    segments = 1;
+  }
+  blobs_per_segment = blobcount / segments;
+  // quadratic coeffs
+  auto *coeffs = new double[segments * 3];
+  if (textord_oldbl_debug) {
+    tprintf(
+        "Linear splining baseline of %d blobs at (%d,%d), into %d segments of "
+        "%d blobs\n",
+        blobcount, box.left(), box.bottom(), segments, blobs_per_segment);
+  }
+  segment = 1;
+  for (index2 = 0; index2 < blobs_per_segment / 2; index2++) {
+    box_next_pre_chopped(&new_it);
+  }
+  index1 = 0;
+  blobindex = index2;
+  do {
+    blobindex += blobs_per_segment;
+    lms.Clear();
+    while (index1 < blobindex || (segment == segments && index1 < blobcount)) {
+      box = box_next_pre_chopped(&blob_it);
+      int middle = (box.left() + box.right()) / 2;
+      lms.Add(ICOORD(middle, box.bottom()));
+      index1++;
+      if (index1 == blobindex - blobs_per_segment / 2 || index1 == blobcount - 1) {
+        xstarts[segment] = box.left();
+      }
+    }
+    lms.Fit(&b, &c);
+    coeffs[segment * 3 - 3] = 0;
+    coeffs[segment * 3 - 2] = b;
+    coeffs[segment * 3 - 1] = c;
+    segment++;
+    if (segment > segments) {
+      break;
+    }
+
+    blobindex += blobs_per_segment;
+    lms.Clear();
+    while (index2 < blobindex || (segment == segments && index2 < blobcount)) {
+      new_box = box_next_pre_chopped(&new_it);
+      int middle = (new_box.left() + new_box.right()) / 2;
+      lms.Add(ICOORD(middle, new_box.bottom()));
+      index2++;
+      if (index2 == blobindex - blobs_per_segment / 2 || index2 == blobcount - 1) {
+        xstarts[segment] = new_box.left();
+      }
+    }
+    lms.Fit(&b, &c);
+    coeffs[segment * 3 - 3] = 0;
+    coeffs[segment * 3 - 2] = b;
+    coeffs[segment * 3 - 1] = c;
+    segment++;
+  } while (segment <= segments);
+  return coeffs;
+}
+
+/**
+ * @name assign_blobs_to_rows
+ *
+ * Make enough rows to allocate all the given blobs to one.
+ * If a block skew is given, use that, else attempt to track it.
+ */
+void assign_blobs_to_rows( // find lines
+    TO_BLOCK *block,       // block to do
+    float *gradient,       // block skew
+    int pass,              // identification
+    bool reject_misses,    // chuck big ones out
+    bool make_new_rows,    // add rows for unmatched
+    bool drawing_skew      // draw smoothed skew
+) {
+  OVERLAP_STATE overlap_result; // what to do with it
+  float ycoord;                 // current y
+  float top, bottom;            // of blob
+  float g_length = 1.0f;        // from gradient
+  int16_t row_count;            // no of rows
+  int16_t left_x;               // left edge
+  int16_t last_x;               // previous edge
+  float block_skew;             // y delta
+  float smooth_factor;          // for new coords
+  float near_dist;              // dist to nearest row
+  ICOORD testpt;                // testing only
+  BLOBNBOX *blob;               // current blob
+  TO_ROW *row;                  // current row
+  TO_ROW *dest_row = nullptr;   // row to put blob in
+                                // iterators
+  BLOBNBOX_IT blob_it = &block->blobs;
+  TO_ROW_IT row_it = block->get_rows();
+
+  ycoord =
+      (block->block->pdblk.bounding_box().bottom() + block->block->pdblk.bounding_box().top()) /
+      2.0f;
+  if (gradient != nullptr) {
+    g_length = std::sqrt(1 + *gradient * *gradient);
+  }
+#ifndef GRAPHICS_DISABLED
+  if (drawing_skew) {
+    to_win->SetCursor(block->block->pdblk.bounding_box().left(), ycoord);
+  }
+#endif
+  testpt = ICOORD(textord_test_x, textord_test_y);
+  blob_it.sort(blob_x_order);
+  smooth_factor = 1.0;
+  block_skew = 0.0f;
+  row_count = row_it.length(); // might have rows
+  if (!blob_it.empty()) {
+    left_x = blob_it.data()->bounding_box().left();
+  } else {
+    left_x = block->block->pdblk.bounding_box().left();
+  }
+  last_x = left_x;
+  for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
+    blob = blob_it.data();
+    if (gradient != nullptr) {
+      block_skew = (1 - 1 / g_length) * blob->bounding_box().bottom() +
+                   *gradient / g_length * blob->bounding_box().left();
+    } else if (blob->bounding_box().left() - last_x > block->line_size / 2 &&
+               last_x - left_x > block->line_size * 2 && textord_interpolating_skew) {
+      //                      tprintf("Interpolating skew from %g",block_skew);
+      block_skew *= static_cast<float>(blob->bounding_box().left() - left_x) / (last_x - left_x);
+      //                      tprintf("to %g\n",block_skew);
+    }
+    last_x = blob->bounding_box().left();
+    top = blob->bounding_box().top() - block_skew;
+    bottom = blob->bounding_box().bottom() - block_skew;
+#ifndef GRAPHICS_DISABLED
+    if (drawing_skew) {
+      to_win->DrawTo(blob->bounding_box().left(), ycoord + block_skew);
+    }
+#endif
+    if (!row_it.empty()) {
+      for (row_it.move_to_first(); !row_it.at_last() && row_it.data()->min_y() > top;
+           row_it.forward()) {
+      }
+      row = row_it.data();
+      if (row->min_y() <= top && row->max_y() >= bottom) {
+        // any overlap
+        dest_row = row;
+        overlap_result = most_overlapping_row(&row_it, dest_row, top, bottom, block->line_size,
+                                              blob->bounding_box().contains(testpt));
+        if (overlap_result == NEW_ROW && !reject_misses) {
+          overlap_result = ASSIGN;
+        }
+      } else {
+        overlap_result = NEW_ROW;
+        if (!make_new_rows) {
+          near_dist = row_it.data_relative(-1)->min_y() - top;
+          // below bottom
+          if (bottom < row->min_y()) {
+            if (row->min_y() - bottom <= (block->line_spacing - block->line_size) *
+                                             tesseract::CCStruct::kDescenderFraction) {
+              // done it
+              overlap_result = ASSIGN;
+              dest_row = row;
+            }
+          } else if (near_dist > 0 && near_dist < bottom - row->max_y()) {
+            row_it.backward();
+            dest_row = row_it.data();
+            if (dest_row->min_y() - bottom <= (block->line_spacing - block->line_size) *
+                                                  tesseract::CCStruct::kDescenderFraction) {
+              // done it
+              overlap_result = ASSIGN;
+            }
+          } else {
+            if (top - row->max_y() <=
+                (block->line_spacing - block->line_size) *
+                    (textord_overlap_x + tesseract::CCStruct::kAscenderFraction)) {
+              // done it
+              overlap_result = ASSIGN;
+              dest_row = row;
+            }
+          }
+        }
+      }
+      if (overlap_result == ASSIGN) {
+        dest_row->add_blob(blob_it.extract(), top, bottom, block->line_size);
+      }
+      if (overlap_result == NEW_ROW) {
+        if (make_new_rows && top - bottom < block->max_blob_size) {
+          dest_row = new TO_ROW(blob_it.extract(), top, bottom, block->line_size);
+          row_count++;
+          if (bottom > row_it.data()->min_y()) {
+            row_it.add_before_then_move(dest_row);
+          // insert in right place
+          } else {
+            row_it.add_after_then_move(dest_row);
+          }
+          smooth_factor = 1.0 / (row_count * textord_skew_lag + textord_skewsmooth_offset);
+        } else {
+          overlap_result = REJECT;
+        }
+      }
+    } else if (make_new_rows && top - bottom < block->max_blob_size) {
+      overlap_result = NEW_ROW;
+      dest_row = new TO_ROW(blob_it.extract(), top, bottom, block->line_size);
+      row_count++;
+      row_it.add_after_then_move(dest_row);
+      smooth_factor = 1.0 / (row_count * textord_skew_lag + textord_skewsmooth_offset2);
+    } else {
+      overlap_result = REJECT;
+    }
+    if (blob->bounding_box().contains(testpt) && textord_debug_blob) {
+      if (overlap_result != REJECT) {
+        tprintf("Test blob assigned to row at (%g,%g) on pass %d\n", dest_row->min_y(),
+                dest_row->max_y(), pass);
+      } else {
+        tprintf("Test blob assigned to no row on pass %d\n", pass);
+      }
+    }
+    if (overlap_result != REJECT) {
+      while (!row_it.at_first() && row_it.data()->min_y() > row_it.data_relative(-1)->min_y()) {
+        row = row_it.extract();
+        row_it.backward();
+        row_it.add_before_then_move(row);
+      }
+      while (!row_it.at_last() && row_it.data()->min_y() < row_it.data_relative(1)->min_y()) {
+        row = row_it.extract();
+        row_it.forward();
+        // Keep rows in order.
+        row_it.add_after_then_move(row);
+      }
+      BLOBNBOX_IT added_blob_it(dest_row->blob_list());
+      added_blob_it.move_to_last();
+      TBOX prev_box = added_blob_it.data_relative(-1)->bounding_box();
+      if (dest_row->blob_list()->singleton() || !prev_box.major_x_overlap(blob->bounding_box())) {
+        block_skew = (1 - smooth_factor) * block_skew +
+                     smooth_factor * (blob->bounding_box().bottom() - dest_row->initial_min_y());
+      }
+    }
+  }
+  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
+    if (row_it.data()->blob_list()->empty()) {
+      delete row_it.extract(); // Discard empty rows.
+    }
+  }
+}
+
+/**
+ * @name most_overlapping_row
+ *
+ * Return the row which most overlaps the blob.
+ */
+OVERLAP_STATE most_overlapping_row( // find best row
+    TO_ROW_IT *row_it,              // iterator
+    TO_ROW *&best_row,              // output row
+    float top,                      // top of blob
+    float bottom,                   // bottom of blob
+    float rowsize,                  // max row size
+    bool testing_blob               // test stuff
+) {
+  OVERLAP_STATE result;          // result of tests
+  float overlap;                 // of blob & row
+  float bestover;                // nearest row
+  float merge_top, merge_bottom; // size of merged row
+  ICOORD testpt;                 // testing only
+  TO_ROW *row;                   // current row
+  TO_ROW *test_row;              // for multiple overlaps
+  BLOBNBOX_IT blob_it;           // for merging rows
+
+  result = ASSIGN;
+  row = row_it->data();
+  bestover = top - bottom;
+  if (top > row->max_y()) {
+    bestover -= top - row->max_y();
+  }
+  if (bottom < row->min_y()) {
+    // compute overlap
+    bestover -= row->min_y() - bottom;
+  }
+  if (testing_blob && textord_debug_blob) {
+    tprintf("Test blob y=(%g,%g), row=(%f,%f), size=%g, overlap=%f\n", bottom, top, row->min_y(),
+            row->max_y(), rowsize, bestover);
+  }
+  test_row = row;
+  do {
+    if (!row_it->at_last()) {
+      row_it->forward();
+      test_row = row_it->data();
+      if (test_row->min_y() <= top && test_row->max_y() >= bottom) {
+        merge_top = std::max(test_row->max_y(),row->max_y());
+        merge_bottom = std::min(test_row->min_y(),row->min_y());
+        if (merge_top - merge_bottom <= rowsize) {
+          if (testing_blob && textord_debug_blob) {
+            tprintf("Merging rows at (%g,%g), (%g,%g)\n", row->min_y(), row->max_y(),
+                    test_row->min_y(), test_row->max_y());
+          }
+          test_row->set_limits(merge_bottom, merge_top);
+          blob_it.set_to_list(test_row->blob_list());
+          blob_it.add_list_after(row->blob_list());
+          blob_it.sort(blob_x_order);
+          row_it->backward();
+          delete row_it->extract();
+          row_it->forward();
+          bestover = -1.0f; // force replacement
+        }
+        overlap = top - bottom;
+        if (top > test_row->max_y()) {
+          overlap -= top - test_row->max_y();
+        }
+        if (bottom < test_row->min_y()) {
+          overlap -= test_row->min_y() - bottom;
+        }
+        if (bestover >= rowsize - 1 && overlap >= rowsize - 1) {
+          result = REJECT;
+        }
+        if (overlap > bestover) {
+          bestover = overlap; // find biggest overlap
+          row = test_row;
+        }
+        if (testing_blob && textord_debug_blob) {
+          tprintf("Test blob y=(%g,%g), row=(%f,%f), size=%g, overlap=%f->%f\n", bottom, top,
+                  test_row->min_y(), test_row->max_y(), rowsize, overlap, bestover);
+        }
+      }
+    }
+  } while (!row_it->at_last() && test_row->min_y() <= top && test_row->max_y() >= bottom);
+  while (row_it->data() != row) {
+    row_it->backward(); // make it point to row
+  }
+                        // doesn't overlap much
+  if (top - bottom - bestover > rowsize * textord_overlap_x &&
+      (!textord_fix_makerow_bug || bestover < rowsize * textord_overlap_x) && result == ASSIGN) {
+    result = NEW_ROW; // doesn't overlap enough
+  }
+  best_row = row;
+  return result;
+}
+
+/**
+ * @name blob_x_order
+ *
+ * Sort function to sort blobs in x from page left.
+ */
+int blob_x_order(      // sort function
+    const void *item1, // items to compare
+    const void *item2) {
+  // converted ptr
+  const BLOBNBOX *blob1 = *reinterpret_cast<const BLOBNBOX *const *>(item1);
+  // converted ptr
+  const BLOBNBOX *blob2 = *reinterpret_cast<const BLOBNBOX *const *>(item2);
+
+  if (blob1->bounding_box().left() < blob2->bounding_box().left()) {
+    return -1;
+  } else if (blob1->bounding_box().left() > blob2->bounding_box().left()) {
+    return 1;
+  } else {
+    return 0;
+  }
+}
+
+/**
+ * @name mark_repeated_chars
+ *
+ * Mark blobs marked with BTFT_LEADER in repeated sets using the
+ * repeated_set member of BLOBNBOX.
+ */
+void mark_repeated_chars(TO_ROW *row) {
+  BLOBNBOX_IT box_it(row->blob_list()); // Iterator.
+  int num_repeated_sets = 0;
+  if (!box_it.empty()) {
+    do {
+      BLOBNBOX *bblob = box_it.data();
+      int repeat_length = 1;
+      if (bblob->flow() == BTFT_LEADER && !bblob->joined_to_prev() && bblob->cblob() != nullptr) {
+        BLOBNBOX_IT test_it(box_it);
+        for (test_it.forward(); !test_it.at_first();) {
+          bblob = test_it.data();
+          if (bblob->flow() != BTFT_LEADER) {
+            break;
+          }
+          test_it.forward();
+          bblob = test_it.data();
+          if (bblob->joined_to_prev() || bblob->cblob() == nullptr) {
+            repeat_length = 0;
+            break;
+          }
+          ++repeat_length;
+        }
+      }
+      if (repeat_length >= kMinLeaderCount) {
+        num_repeated_sets++;
+        for (; repeat_length > 0; box_it.forward(), --repeat_length) {
+          bblob = box_it.data();
+          bblob->set_repeated_set(num_repeated_sets);
+        }
+      } else {
+        bblob->set_repeated_set(0);
+        box_it.forward();
+      }
+    } while (!box_it.at_first()); // until all done
+  }
+  row->set_num_repeated_sets(num_repeated_sets);
+}
+
+} // namespace tesseract