FOSSredder 0.5.0
Loading...
Searching...
No Matches
DefaultStatementParser.cpp
Go to the documentation of this file.
1
5
6#include "core/pch.h"
7
9
16
17#include <algorithm>
18#include <optional>
19#include <regex>
20
22
23namespace helpers = core::application::importing::internal;
24
25using core::application::importing::transaction::DefaultTransactionParser;
28
29namespace {
30
31static bool isValutaHeaderLine(const std::string& line) {
32 const auto n = helpers::normalizeAlnumLower(line);
33 return n.find("valuta") != std::string::npos;
34}
35
36struct RowModel {
37 bool inSection = false;
38};
39
40using helpers::isFooterLine;
41using helpers::isHeaderNoiseLine;
42using helpers::isPostTransactionFootnote;
43using helpers::isTransactionsSectionHeader;
44using helpers::isDebitCreditHeaderLine;
45
46static bool isLikelyTransactionMainRowText(const std::string& line) {
47 static const std::regex re(R"((\d{2}\.\s*\d{2})\s+\d{1,3}(?:[\.,]\d{3})*[\.,]\d{1,2}-?(?:\s+.*)?\s*$)");
48 return std::regex_search(line, re);
49}
50
51}
52
55 const std::string& pageCropImagePath,
56 std::shared_ptr<core::ports::document_image_processing::IDocumentImageProcessor> documentImageProcessor,
57 const std::vector<uint8_t>& pageCropImageBytes,
58 std::string initialBookingDate,
59 int initialTransactionIndex) {
60 (void)table;
61 ParseResult out;
62
63 out.debugLines.push_back(std::string("pageCropImagePath\t") + pageCropImagePath);
64 out.debugLines.push_back(std::string("pageCropImageBytes\t") + std::to_string(pageCropImageBytes.size()));
65 out.debugLines.push_back(std::string("initialBookingDate\t") + initialBookingDate);
66 out.debugLines.push_back(std::string("initialTransactionIndex\t") + std::to_string(initialTransactionIndex));
67
68 const auto ocrLines = helpers::buildOcrLinesFromWords(ocr.words);
69 out.debugLines.push_back(std::string("ocr.words\t") + std::to_string(ocr.words.size()));
70 out.debugLines.push_back(std::string("ocr.lines.raw\t") + std::to_string(ocrLines.size()));
71
72 const auto seed = helpers::inferColumnModelFromLines(ocrLines);
73 core::application::importing::statement::internal::ColumnModel seedCols{ seed.valutaX, seed.debitX, seed.creditX, -1, -1, -1 };
74
75 std::vector<core::application::importing::internal::RawLineLite> rawLite;
76 rawLite.reserve(ocrLines.size());
77 for (const auto& l : ocrLines) rawLite.push_back({ l.minX, l.maxX, l.minY, l.maxY, l.wordSpans, l.text });
78
80 std::vector<core::application::importing::statement::internal::RawLine> lines;
81 lines.reserve(merged.size());
82 for (const auto& ml : merged) lines.push_back(core::application::importing::statement::internal::RawLine{0, ml.minX, ml.maxX, ml.minY, ml.maxY, ml.wordSpans, ml.text});
83 out.debugLines.push_back(std::string("ocr.lines\t") + std::to_string(lines.size()));
84
85 std::vector<core::application::importing::transaction::internal::OcrLine> ocrFromRaw;
86 ocrFromRaw.reserve(lines.size());
87 for (const auto& rl : lines) ocrFromRaw.push_back(core::application::importing::statement::internal::rawToOcrLine(rl));
88
89 std::vector<std::pair<size_t,std::string>> pageHeaderDates;
90 try {
91 for (size_t i = 0; i < lines.size(); ++i) {
92 try {
94 pageHeaderDates.emplace_back(i, *bd);
95 }
96 } catch (...) { }
97 }
98 } catch (...) { }
99
100 std::string currentBookingDate = std::move(initialBookingDate);
101 int txIndex = std::max(1, initialTransactionIndex);
102
103 size_t headerScanPtr = 0;
104
105 {
106 std::string dbg;
108 out.debugLines.push_back(dbg);
109 out.debugLines.push_back(std::string("page.emptyDetected\tlines=") + std::to_string(12));
110 out.lastBookingDate = currentBookingDate;
111 out.nextTransactionIndex = txIndex;
112 return out;
113 }
114 if (!dbg.empty()) out.debugLines.push_back(dbg);
115 }
116
117 std::vector<core::application::importing::transaction::internal::TransactionBlock> blocks;
119 cur.bookingDateGroup = currentBookingDate;
120
121 bool inTransactions = !currentBookingDate.empty();
123 RowModel rows;
124
126 static constexpr size_t headerScanLines = 12;
127 int headerBottomY = headerAnalysis.headerBottomY;
128 const int headerMarginPx = headerAnalysis.headerMarginPx;
129 const int pageMaxY = headerAnalysis.pageMaxY;
130 const bool headerFound = headerAnalysis.headerFound;
131
132 if (currentBookingDate.empty()) {
134 currentBookingDate = *d;
135 out.debugLines.push_back(std::string("header.defaultBookingDate\t") + currentBookingDate);
136 cur.bookingDateGroup = currentBookingDate;
137 }
138 }
139
140 const auto flush = [&]() {
141 if (cur.main.left.empty() && cur.detailLines.empty()) return;
142 blocks.push_back(std::move(cur));
144 cur.bookingDateGroup = currentBookingDate;
145 };
146
147 std::string prevLine;
148 int txStartLooseCount = 0;
149
150 std::vector<core::application::importing::transaction::internal::OcrLine> orphanLines;
151
152 for (size_t li = 0; li < lines.size(); ++li) {
153 const auto& l = lines[li];
154 const auto txt = l.text;
155 const auto combined = prevLine.empty() ? txt : (prevLine + " " + txt);
156 if (txt.empty()) continue;
157
158 if (headerScanPtr < pageHeaderDates.size() && pageHeaderDates[headerScanPtr].first == li) {
159 currentBookingDate = pageHeaderDates[headerScanPtr].second;
160 out.debugLines.push_back(std::string("header.foundOnPage\tline=") + std::to_string(li) + std::string("\t") + currentBookingDate);
161 ++headerScanPtr;
162 inTransactions = true;
163 flush();
164 cur.bookingDateGroup = currentBookingDate;
165 prevLine = txt;
166 continue;
167 }
168 while (headerScanPtr < pageHeaderDates.size() && pageHeaderDates[headerScanPtr].first < li) ++headerScanPtr;
169
170 if (isFooterLine(txt)) { out.debugLines.push_back(std::string("stop.footer\t") + txt + "\tline=" + std::to_string(li)); break; }
171 if (isPostTransactionFootnote(txt)) {
172 bool nearBottom = false;
173 try { if (pageMaxY >= 0 && l.maxY >= static_cast<int>(pageMaxY * 3 / 4)) nearBottom = true; } catch (...) { }
174 try {
175 out.debugLines.push_back(std::string("footnote.check\tpageMaxY=") + std::to_string(pageMaxY) + std::string("\tlineY=") + std::to_string(l.maxY) + std::string("\tnearBottom=") + (nearBottom ? "1" : "0") + std::string("\tblocks=") + std::to_string(blocks.size()));
176 } catch (...) { }
177
178 bool reallyTerminal = false;
179 try { if (!blocks.empty() && nearBottom) reallyTerminal = true; } catch (...) { }
180 if (reallyTerminal) {
181 out.debugLines.push_back(std::string("stop.footnote\t") + txt + "\tline=" + std::to_string(li));
182 break;
183 }
184 out.debugLines.push_back(std::string("footnote.ignored\t") + txt + "\tline=" + std::to_string(li));
185 prevLine = txt;
186 continue;
187 }
188
189 if (headerBottomY >= 0 && l.maxY <= headerBottomY + headerMarginPx && !inTransactions) {
190 try {
191 if (seedCols.valutaX >= 0) {
193 if (helpers::hasTokenNearX(anchorOl, seedCols.valutaX, helpers::parserConfig.tokenNearBandForMainRow) || helpers::hasAmountNearValuta(anchorOl, seedCols.valutaX, helpers::parserConfig.amountNearValutaBandPx)) {
194 out.debugLines.push_back(std::string("header.overrideByValutaAnchor\tline=") + std::to_string(li) + "\ttext=" + txt);
195 } else {
196 continue;
197 }
198 }
199 } catch (...) { }
200
201 bool hasHeaderSignal = false;
202 try {
203 if (core::application::importing::statement::internal::findBookingDateInHeader(txt).has_value()) hasHeaderSignal = true;
204 } catch (...) { }
205 try {
206 if (core::application::importing::statement::internal::findBookingDateInHeader(combined).has_value()) hasHeaderSignal = true;
207 } catch (...) { }
208 try {
209 if (isValutaHeaderLine(txt)) hasHeaderSignal = true;
210 } catch (...) { }
211
212 if (!hasHeaderSignal && !(rows.inSection && cols.hasValuta())) {
213 bool combinedStarted = false;
214 try {
215 std::string combPrev = prevLine.empty() ? txt : (prevLine + std::string(" ") + txt);
216 std::string combNext = txt;
217 if (li + 1 < lines.size()) combNext = txt + std::string(" ") + lines[li+1].text;
218 try {
220 inTransactions = true;
221 out.debugLines.push_back(std::string("tx.start.combined.aboveHeader\tline=") + std::to_string(li) + "\ttext=" + (cs->second ? combNext : combPrev));
222 combinedStarted = true;
223 }
224 } catch (...) { }
225 } catch (...) { }
226 if (combinedStarted) {
227 } else {
228 try {
230 bool allowAboveHeader = false;
231 try { if (helpers::hasAmountNearValuta(ol, cols.valutaX, helpers::parserConfig.amountNearValutaBandPx)) allowAboveHeader = true; } catch (...) { }
232 try { if (!allowAboveHeader && helpers::isLooseTransactionLine(ol, cols.valutaX)) allowAboveHeader = true; } catch (...) { }
233 if (allowAboveHeader) {
234 inTransactions = true;
235 ++txStartLooseCount;
236 out.debugLines.push_back(std::string("tx.start.aboveHeaderDetected\tline=") + std::to_string(li) + "\ttext=" + txt);
237 } else {
238 out.debugLines.push_back(std::string("line.reason\taboveHeaderSkip\t") + txt + "\tline=" + std::to_string(li));
239 prevLine = txt;
240 continue;
241 }
242 } catch (...) {
243
244 out.debugLines.push_back(std::string("line.reason\taboveHeaderSkip\t") + txt + "\tline=" + std::to_string(li));
245 prevLine = txt;
246 continue;
247 }
248 }
249 }
250 }
251
253 if (!cols.hasDebit()) {
254 if (auto dx = helpers::findPhraseCenterX(core::application::importing::statement::internal::rawToOcrLine(l), {"zu","ihren","lasten"})) cols.debitX = *dx;
255 }
256 if (!cols.hasCredit()) {
257 if (auto cx = helpers::findPhraseCenterX(core::application::importing::statement::internal::rawToOcrLine(l), {"zu","ihren","gunsten"})) cols.creditX = *cx;
258 }
259 out.debugLines.push_back(std::string("header.debitcredit\t") + txt + "\tline=" + std::to_string(li));
260 out.debugLines.push_back(std::string("cols.debitX\t") + std::to_string(cols.debitX) + "\tcols.creditX\t" + std::to_string(cols.creditX));
261 try { headerBottomY = std::max(headerBottomY, l.maxY); } catch (...) { }
262 prevLine = txt;
263 continue;
264 }
265
266 if (!rows.inSection && (helpers::isTransactionsSectionHeader(txt) || isValutaHeaderLine(txt) || helpers::isTransactionsSectionHeader(combined))) {
267 rows.inSection = true;
268 if (!cols.hasValuta()) {
269 if (auto vx = helpers::findTokenCenterX(core::application::importing::statement::internal::rawToOcrLine(l), "valuta")) cols.valutaX = *vx;
270 }
271 out.debugLines.push_back(std::string("header.section\t") + combined + "\tline=" + std::to_string(li));
272 out.debugLines.push_back(std::string("cols.valutaX\t") + std::to_string(cols.valutaX));
273 try { headerBottomY = std::max(headerBottomY, l.maxY); } catch (...) { }
274 prevLine = txt;
275 continue;
276 }
277
279 currentBookingDate = *bd;
280 out.debugLines.push_back(std::string("header.bookingDate\t") + currentBookingDate + "\tline=" + std::to_string(li));
281 flush();
282 cur.bookingDateGroup = currentBookingDate;
283 try { headerBottomY = std::max(headerBottomY, l.maxY); } catch (...) { }
284 prevLine = txt;
285 continue;
286 }
287
288 bool headerNoiseSkipped = false;
289 try {
290 if (!inTransactions && headerFound && helpers::isHeaderNoiseLine(txt)) {
291 bool anchoredToValuta = false;
292 try { if (seedCols.valutaX >= 0) { core::application::importing::transaction::internal::OcrLine _ol = core::application::importing::statement::internal::rawToOcrLine(l); if (helpers::hasTokenNearX(_ol, seedCols.valutaX, helpers::parserConfig.tokenNearBandForMainRow) || helpers::hasAmountLikeTokenInLine(_ol, seedCols.valutaX)) anchoredToValuta = true; } } catch (...) { }
293
294 bool looksMain = false;
295 try {
297 if (helpers::isLooseTransactionLine(_ol, seedCols.valutaX)) looksMain = true;
298 if (helpers::hasAmountLikeTokenInLine(_ol, seedCols.valutaX)) looksMain = true;
299 if (helpers::hasShortDateToken(_ol.text) && helpers::hasLeftDescriptiveText(_ol, seedCols.valutaX)) looksMain = true;
300 } catch (...) { }
301
302 if (!anchoredToValuta && !looksMain && !isTransactionsSectionHeader(txt) && !isDebitCreditHeaderLine(txt) && !isLikelyTransactionMainRowText(txt)) {
303 out.debugLines.push_back(std::string("line.skip.headerNoise\t") + txt + std::string("\tline=") + std::to_string(li));
304 prevLine = txt;
305 headerNoiseSkipped = true;
306 } else {
307 out.debugLines.push_back(std::string("header.noise_filter.suppressed\tline=") + std::to_string(li));
308 }
309 }
310 } catch (...) { }
311 if (headerNoiseSkipped) continue;
312
313 if (!inTransactions) {
314 const bool canStart = (!currentBookingDate.empty()) || (rows.inSection && cols.hasValuta());
315 bool didVerticalStart = false;
316 try {
317 if (canStart && rows.inSection && cols.hasValuta()) {
318 if (auto vs = core::application::importing::statement::internal::tryVerticalStart(ocrFromRaw, li, { cols.valutaX, cols.debitX, cols.creditX })) {
319 inTransactions = true; didVerticalStart = true;
320 if (!cur.main.left.empty() || !cur.detailLines.empty()) flush();
321 cur.bookingDateGroup = currentBookingDate;
322 cur.main = vs->first;
323 if (vs->second > 0) {
324 out.debugLines.push_back(std::string("tx.main.vertical.helper\tformed\tline=") + std::to_string(li));
325 ++li;
326 } else {
327 out.debugLines.push_back(std::string("tx.main.vertical.helper\tformed\tline=") + std::to_string(li));
328 }
329 }
330 }
331 } catch (...) { }
332
333 if (canStart && (isLikelyTransactionMainRowText(txt) || isLikelyTransactionMainRowGeom(l, cols))) {
334 inTransactions = true;
335 out.debugLines.push_back(std::string("tx.start\tline=") + std::to_string(li) + "\ttext=" + txt);
336 } else if (didVerticalStart) {
337 } else {
338 bool startedCombined = false;
339 try {
340 std::string combPrev = prevLine.empty() ? txt : (prevLine + std::string(" ") + txt);
341 std::string combNext = txt;
342 if (li + 1 < lines.size()) combNext = txt + std::string(" ") + lines[li+1].text;
343 try {
344 if (auto cs = core::application::importing::statement::internal::tryCombinedStart(ocrFromRaw, li, { cols.valutaX, cols.debitX, cols.creditX })) {
345 inTransactions = true; startedCombined = true;
346 out.debugLines.push_back(std::string("tx.start.combined\tline=") + std::to_string(li) + "\ttext=" + (cs->second ? combNext : combPrev));
347 }
348 } catch (...) { }
349 } catch (...) { }
350 if (startedCombined) {
351 } else {
352 bool relaxedStart = false;
353 try {
355 bool hasDate = false;
356 try { if (helpers::hasShortDateToken(ol.text)) hasDate = true; } catch (...) { }
357 bool hasAmount = false;
358 try { if (helpers::hasAmountNearValuta(ol, cols.valutaX, helpers::parserConfig.amountNearValutaBandPx)) hasAmount = true; } catch (...) { }
359 try {
360 if (!hasAmount && li > 0) { auto prevOl = core::application::importing::statement::internal::rawToOcrLine(lines[li-1]); if (helpers::hasAmountNearValuta(prevOl, cols.valutaX, helpers::parserConfig.amountNearValutaBandPx)) hasAmount = true; }
361 if (!hasAmount && li + 1 < lines.size()) { auto nextOl = core::application::importing::statement::internal::rawToOcrLine(lines[li+1]); if (helpers::hasAmountNearValuta(nextOl, cols.valutaX, helpers::parserConfig.amountNearValutaBandPx)) hasAmount = true; }
362 } catch (...) { }
363 bool hasLeftDesc = false;
364 try { if (helpers::hasLeftDescriptiveText(ol, cols.valutaX)) hasLeftDesc = true; } catch (...) { }
365 if (hasDate && (hasAmount || hasLeftDesc)) relaxedStart = true;
366 } catch (...) { }
367 if (relaxedStart) {
368 inTransactions = true;
369 out.debugLines.push_back(std::string("tx.start.relaxed\tline=") + std::to_string(li) + "\ttext=" + txt);
370 } else {
371 inTransactions = true;
372 out.debugLines.push_back(std::string("tx.start.forced\tline=") + std::to_string(li) + "\ttext=" + txt);
373 if (!cur.main.left.empty() || !cur.detailLines.empty()) flush();
374 cur.bookingDateGroup = currentBookingDate;
377 prevLine = txt;
378 continue;
379 }
380 }
381 }
382 }
383
384 const bool mainByRegex = isLikelyTransactionMainRowText(txt);
385 core::application::importing::statement::internal::ColumnModel effectiveCols = cols.hasValuta() ? cols : seedCols;
386 const bool mainByGeom = (!mainByRegex) && ((li > headerScanLines) || effectiveCols.hasValuta()) && core::application::importing::statement::internal::isLikelyTransactionMainRowGeom(l, effectiveCols);
387
388 if (mainByRegex || mainByGeom) {
389 if (!cur.main.left.empty() || !cur.detailLines.empty()) flush();
390 cur.bookingDateGroup = currentBookingDate;
393 prevLine = txt;
394 continue;
395 }
396
397 if (!cur.main.left.empty()) {
400 }
401
402 prevLine = txt;
403 }
404
405 flush();
406
407 try {
408 for (const auto& ol : orphanLines) {
409 try {
410 bool looksMain = false;
411 try { if (core::application::importing::internal::hasAmountLikeTokenInLine(ol, cols.valutaX)) looksMain = true; } catch (...) { }
412 try { if (!looksMain && core::application::importing::internal::isLooseTransactionLine(ol, cols.valutaX)) looksMain = true; } catch (...) { }
413 try { if (!looksMain && core::application::importing::internal::hasShortDateToken(ol.text) && core::application::importing::internal::hasLeftDescriptiveText(ol, cols.valutaX)) looksMain = true; } catch (...) { }
414 if (looksMain) {
416 nb.bookingDateGroup = currentBookingDate;
419 blocks.push_back(std::move(nb));
420 out.debugLines.push_back(std::string("tx.start.rescued\ttext=") + ol.text);
421 }
422 } catch (...) { }
423 }
424 } catch (...) { }
425
427 orphanLines,
428 helpers::parserConfig.orphanAttachMaxGapPx,
429 cols.valutaX,
430 &out.debugLines);
431
432 out.debugLines.push_back(std::string("blocks\t") + std::to_string(blocks.size()));
433
434 core::application::importing::statement::internal::appendPageSummary(lines, headerBottomY, blocks, txStartLooseCount, out);
435 core::application::importing::statement::internal::appendTransactionsFromBlocks(blocks, cols, ocr, documentImageProcessor, pageCropImagePath, pageCropImageBytes, txIndex, out);
436
437 out.debugLines.push_back(std::string("transactions\t") + std::to_string(out.transactions.size()));
438
439 out.lastBookingDate = currentBookingDate;
440 out.nextTransactionIndex = txIndex;
441 return out;
442}
443
444}
Declares the public default statement parser used by the import pipeline.
Declares the default transaction parser used by the import pipeline.
Declares runtime configuration values for the import parsers.
Declares low-level OCR helper routines for statement parsing.
Declares heuristics used to classify statement parser lines.
Declares helpers for the statement parsing workflow.
Declares private string helpers shared across core implementation files.
static ParseResult parse(const core::ports::document_image_processing::Table &table, const core::ports::text_recognition::ExtractResult &ocr, const std::string &pageCropImagePath, std::shared_ptr< core::ports::document_image_processing::IDocumentImageProcessor > documentImageProcessor, const std::vector< uint8_t > &pageCropImageBytes, std::string initialBookingDate={}, int initialTransactionIndex=1)
Parses an OCR-backed table into transaction drafts and artifacts.
Collects common core includes and aliases for precompiled-header builds.
bool isLooseTransactionLine(const core::application::importing::transaction::internal::OcrLine &line, int valutaX) noexcept
Checks whether the line looks like a loosely formatted transaction row.
bool hasAmountLikeTokenInLine(const core::application::importing::transaction::internal::OcrLine &line, int valutaX) noexcept
Checks whether the line contains amount-like tokens near the valuta column.
bool hasLeftDescriptiveText(const core::application::importing::transaction::internal::OcrLine &line, int valutaX) noexcept
Checks whether the line contains descriptive text to the left of the valuta column.
bool hasShortDateToken(const std::string &text) noexcept
Checks whether the text contains a short date token.
void appendPageSummary(const std::vector< core::application::importing::statement::internal::RawLine > &lines, int headerBottomY, const std::vector< core::application::importing::transaction::internal::TransactionBlock > &blocks, int txStartLooseCount, core::application::importing::statement::DefaultStatementParser::ParseResult &out)
Appends a page-level summary to the parser result.
bool isLikelyTransactionMainRowGeom(const core::application::importing::statement::internal::RawLine &line, const core::application::importing::statement::internal::ColumnModel &cols)
Checks whether a raw line likely contains a main transaction row.
HeaderAnalysis analyzeHeaderWindow(const std::vector< core::application::importing::transaction::internal::OcrLine > &ocrLines, const std::vector< core::application::importing::statement::internal::RawLine > &lines, const core::application::importing::statement::internal::ColumnModel &seedCols, core::application::importing::statement::DefaultStatementParser::ParseResult &out)
Analyzes the page header window and updates parser output state.
core::application::importing::transaction::internal::OcrLine rawToOcrLine(const core::application::importing::statement::internal::RawLine &line)
Converts a raw OCR line into a parser line model.
void attachOrphansToBlocks(std::vector< core::application::importing::transaction::internal::TransactionBlock > &blocks, const std::vector< core::application::importing::transaction::internal::OcrLine > &orphans, int maxGapPx, int valutaX, std::vector< std::string > *debugOut=nullptr) noexcept
Attaches orphan OCR lines to existing transaction blocks.
std::optional< std::string > findBookingDateInHeader(const std::string &line) noexcept
Extracts a booking date from a header line.
void appendDetailLine(core::application::importing::transaction::internal::TransactionBlock &cur, const core::application::importing::transaction::internal::OcrLine &line, const core::application::importing::internal::ColumnGuess &cols, std::vector< std::string > *debugOut=nullptr) noexcept
Appends a detail line to the current transaction block.
std::optional< std::pair< core::application::importing::transaction::internal::TransactionMainRow, int > > tryVerticalStart(const std::vector< core::application::importing::transaction::internal::OcrLine > &lines, size_t li, const core::application::importing::internal::ColumnGuess &cols) noexcept
Attempts to start a transaction block from a vertically aligned line.
std::optional< std::string > findDefaultBookingDate(const std::vector< core::application::importing::transaction::internal::OcrLine > &lines, size_t scanLines) noexcept
Finds a default booking date near the top of the parsed page.
bool detectEarlyEmptyPage(const std::vector< core::application::importing::transaction::internal::OcrLine > &lines, std::string &outDebug) noexcept
Detects whether a page is effectively empty before parsing continues.
std::vector< core::application::importing::internal::RawLineLite > selectiveGroupMergeLinesRaw(const std::vector< core::application::importing::internal::RawLineLite > &lines, int maxGapPx, const core::application::importing::internal::ColumnGuess &seedCols)
Merges nearby raw lines using the parser's selective grouping heuristic.
bool isLikelyTransactionHeaderLine(const core::application::importing::transaction::internal::OcrLine &line, const core::application::importing::statement::internal::ColumnModel &cols)
Checks whether an OCR line likely represents a transaction header row.
core::application::importing::transaction::internal::TransactionMainRow handleMainRow(const core::application::importing::transaction::internal::OcrLine &line, const core::application::importing::internal::ColumnGuess &cols, bool isGeom, std::vector< std::string > &debugOut) noexcept
Converts one OCR line into a transaction main row.
void appendTransactionsFromBlocks(const std::vector< core::application::importing::transaction::internal::TransactionBlock > &blocks, const core::application::importing::statement::internal::ColumnModel &cols, const core::ports::text_recognition::ExtractResult &ocr, const std::shared_ptr< core::ports::document_image_processing::IDocumentImageProcessor > &documentImageProcessor, const std::string &pageCropImagePath, const std::vector< uint8_t > &pageCropImageBytes, int &txIndex, core::application::importing::statement::DefaultStatementParser::ParseResult &out)
Converts parsed transaction blocks into drafts and extraction artifacts.
std::optional< std::pair< core::application::importing::transaction::internal::TransactionMainRow, int > > tryCombinedStart(const std::vector< core::application::importing::transaction::internal::OcrLine > &lines, size_t li, const core::application::importing::internal::ColumnGuess &cols) noexcept
Attempts to start a transaction block from a combined text pattern.
std::string lowerAscii(std::string s)
Converts ASCII letters in a string to lowercase.
std::string trim(std::string s)
Removes leading and trailing ASCII whitespace from a string.
Stores the inferred column positions for an OCR page.
std::vector< core::application::importing::draft::TransactionDraft > transactions
Represents the inferred transaction column layout for a page.
Holds the current header-detection state for one statement page.
Represents a merged raw OCR line before it is turned into parser lines.
Represents one OCR line with its bounding box and recognized text.
Groups the main row and detail rows of one parsed transaction block.