5.2 KiB
Rule-Based OCR Parser Architecture
Reference for the parseWithRules() parser in appointments.html (ShopProQuote). Extracts structured appointment data from Tesseract.js OCR output — no AI/API, pure JavaScript regex, runs entirely in the browser.
Architecture Overview
Raw OCR text
→ Phase 1: Normalize & Clean (strip headers, separators, metadata)
→ Phase 2: Block Splitting (row-anchor primary, blank-line fallback, table fallback)
→ Phase 3: parseOne() recursive extraction per block
→ Output: array of {customerName, customerPhone, customerEmail, vin, vehicleInfo, serviceType, notes}
Phase 2 — Row-Anchor Splitting (Primary)
Instead of splitting on \n\n (fragile — Tesseract's horizontal table scan often produces text with inconsistent blank lines), split on Time + Duration patterns:
var rowAnchorRe = /(\d{1,2}):(\d{2})\s*(AM|PM)\s+(\d+(?:\.\d+)?)\s*hrs?/gi;
Find all anchor positions, slice text at boundaries. Each chunk starts with a time+duration — one appointment per chunk.
Fallback: if < 2 anchors found, revert to \n\n blank-line splitting.
Phase 3 — parseOne() Consume-and-Destroy Order
Critical: extraction order is strict. Each field is matched and removed from the text before the next field is searched — prevents false matches.
1a. Time (anchor) — extract first, remove
1b. Duration — minutes/hours, remove
1c. Phone — 10-digit, any format, remove
1d. Email — extract before VIN (prevents email digits matching VIN/date)
1e. VIN — 17-char boundary, OCR-tolerant (O→0, I→1, Q→0), remove
1f. Post-VIN cleanup — strip residual "VIN" label, standalone "RO" token
1g. Advisor sanitize — hard-strip "Rrahman Grajqevci" and variants
1h. Date — 3 formats: YYYY-MM-DD, M/D/YYYY, "Mon DD, YYYY"
After structured extraction, remaining text parts are split on \s{2,} OR \n (preserves multi-line OCR structure — do NOT collapse newlines to spaces before splitting).
Advisors / RO Codes
Pattern: AdvisorName [RO_CODE] is stripped from parts. The [CODE] is captured into notes as RO: [CODE].
Cross-Block Name Carry
Trailing ALL-CAPS names at the bottom of a block belong to the NEXT block. Parser detects via serviceType.match(/\s+([A-Z]{2,}(?:\s+[A-Z]{2,}){1,2})\s*$/) and passes as carryName to the next parseOne() call.
Vehicle Extraction
Primary: find part matching year + make (e.g., "2023 Honda")
Fallback: find part matching year OR make alone
Model capture: if next part is ALL-CAPS alphanumeric and not a service keyword, append to vehicle
Header Metadata Stripping
Before block splitting, lines matching schedule headers are removed:
- Day-of-week + date:
Wednesday Jun 18, 2025 - Single-word labels:
Weekly,Advisor,Refresh,Daily
Key Regex Patterns
| Field | Pattern | Notes |
|---|---|---|
| Phone | \(?\d{3}\)?[\s.\-]*\d{3}[\s.\-]*\d{4} |
Any format |
| VIN | \b[A-HJ-NPR-Z0-9OIQ]{17}\b/i |
OCR-tolerant, word-bounded |
\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b |
Extracted pre-VIN | |
| Date | (\d{4})[\/\-.](\d{1,2})[\/\-.](\d{1,2}) |
ISO format primary |
| Time | `\b(\d{1,2}):(\d{2})\s*(AM | PM)?\b` |
| Duration | `\b(\d+(?:.\d+)?)\s*(?:min | minutes? |
| Row Anchor | `(\d{1,2}):(\d{2})\s*(AM | PM)\s+(\d+(?:.\d+)?)\s*hrs?` |
| Makes | 25+ automotive brands | Used in vehicle detection |
| Year | `\b(19 | 20)\d{2}\b` |
Pitfalls
- Double-escape in regex strings: When editing regex in HTML patches,
\\din the file is two characters. Using\\\\dproduces four. Verify withgrep -n '\\\\\\\\d'after editing. - Don't collapse newlines in block processing: The block-iteration step must NOT do
.replace(/\n/g, ' '). Parts splitting uses\nas a delimiter; collapsing them first destroys multi-line structure. - "RO" token contamination: The text "RO Gary Bowers" has standalone "RO" before the name. Strip
\bRO\bafter VIN extraction, before advisor stripping. - "VIN" label residue: After VIN extraction, the word "VIN" remains. Strip
\bVIN\bin post-VIN cleanup. - Email false positives: Email addresses contain digits (e.g.,
garyb9623@gmail.com). Extract email BEFORE VIN to prevent the digits matching VIN patterns. - Table fallback only fires when blank-line split produces 1 block. The row-anchor path runs first and can produce multiple blocks — table fallback is a separate code path for the
blocks.length === 1case.
Test Suite
7 test cases in the parser test harness cover:
- Two appointments with blank-line separation
- Shop header + separator lines
- Single appointment
- Table format (uniform columns)
- Real 3-appointment screenshot with blank lines + email + advisor
- OCR horizontal-scan format with NO blank lines (row-anchor test)
- Email/code false-positive guard
Run tests via:
cd /mnt/seagate8tb/Websites/ShopProQuote
sed -n '1147,1363p' appointments.html > /tmp/parser_fn.js
echo 'module.exports = parseWithRules;' >> /tmp/parser_fn.js
node -e "var p = require('/tmp/parser_fn.js'); /* ... test cases ... */"