Files
2026-07-12 10:17:17 -04:00

105 lines
5.2 KiB
Markdown

# 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**:
```javascript
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 |
| Email | `\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` | With/without meridian |
| Duration | `\b(\d+(?:\.\d+)?)\s*(?:min|minutes?|hrs?|hours?)\b` | Decimal hours OK |
| Row Anchor | `(\d{1,2}):(\d{2})\s*(AM|PM)\s+(\d+(?:\.\d+)?)\s*hrs?` | Global flag, used for block splitting |
| Makes | 25+ automotive brands | Used in vehicle detection |
| Year | `\b(19|20)\d{2}\b` | Model years 1900-2099 |
## Pitfalls
- **Double-escape in regex strings:** When editing regex in HTML patches, `\\d` in the file is two characters. Using `\\\\d` produces four. Verify with `grep -n '\\\\\\\\d'` after editing.
- **Don't collapse newlines in block processing:** The block-iteration step must NOT do `.replace(/\n/g, ' ')`. Parts splitting uses `\n` as 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\b` after VIN extraction, before advisor stripping.
- **"VIN" label residue:** After VIN extraction, the word "VIN" remains. Strip `\bVIN\b` in 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 === 1` case.
## Test Suite
7 test cases in the parser test harness cover:
1. Two appointments with blank-line separation
2. Shop header + separator lines
3. Single appointment
4. Table format (uniform columns)
5. Real 3-appointment screenshot with blank lines + email + advisor
6. OCR horizontal-scan format with NO blank lines (row-anchor test)
7. Email/code false-positive guard
Run tests via:
```bash
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 ... */"
```