Skip to content

Commit c117b1f

Browse files
committed
fix: preserve locality and unit evidence without header noise
1 parent 901eff2 commit c117b1f

3 files changed

Lines changed: 105 additions & 14 deletions

File tree

‎docs/general-address-reliability-2026-10-19.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,21 @@ or the street-to-city join. Nearby uncertain unit fragments remain evidence and
5050
still require review. Strict consensus packet validation and all three engines
5151
remain intact.
5252

53+
Only validated state codes form standalone state boundaries. Unknown short OCR
54+
fragments cannot prematurely terminate the following city line. Backward evidence
55+
collection requires vertical overlap, or an adjacent nearby plausible unit token
56+
(mixed letters/digits or an explicit unit marker, bounded to 32 characters).
57+
This keeps unrelated header artifacts out of the first address while preserving
58+
uncertain unit evidence.
59+
5360
Geocoding `street-address-v7` invalidates older location metadata in cache/session
5461
validation. Recovered rows show Google's map address and retain the original
5562
address text. Unit verification and building-only verification remain distinct;
5663
the normal retry/loading/success feedback and uncertain-command holds remain.
5764

5865
## Validation and release
5966

60-
- Clean package: 228 Java tests passed, including 36 name/state/unit combinations
67+
- Clean package: 231 Java tests passed, including 36 name/state/unit combinations
6168
and rejection/proof-budget tests. All 36 frontend tests passed, including v5/v6
6269
saved-session migration and safe rendering of provider text.
6370
- Eleven user-authorized Google diagnostics were recorded privately. Replaying

‎src/main/java/com/jtdev/routelisttotesla/service/AddressOcrService.java‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ public class AddressOcrService {
6666
private static final Pattern STATE_SUFFIX = Pattern.compile(
6767
"(?i)(?:^|[,\\s])(?:AL|AK|AZ|AR|CA|CO|CT|DE|DC|FL|GA|HI|ID|IL|IN|IA|KS|KY|LA|ME|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|NJ|NM|NY|NC|ND|OH|OK|OR|PA|RI|SC|SD|TN|TX|UT|VT|VA|WA|WV|WI|WY|PR|GU|VI|AS|MP|ALABAMA|ALASKA|ARIZONA|ARKANSAS|CALIFORNIA|COLORADO|CONNECTICUT|DELAWARE|FLORIDA|GEORGIA|HAWAII|IDAHO|ILLINOIS|INDIANA|IOWA|KANSAS|KENTUCKY|LOUISIANA|MAINE|MARYLAND|MASSACHUSETTS|MICHIGAN|MINNESOTA|MISSISSIPPI|MISSOURI|MONTANA|NEBRASKA|NEVADA|NEW\\s+HAMPSHIRE|NEW\\s+JERSEY|NEW\\s+MEXICO|NEW\\s+YORK|NORTH\\s+CAROLINA|NORTH\\s+DAKOTA|OHIO|OKLAHOMA|OREGON|PENNSYLVANIA|RHODE\\s+ISLAND|SOUTH\\s+CAROLINA|SOUTH\\s+DAKOTA|TENNESSEE|TEXAS|UTAH|VERMONT|VIRGINIA|WASHINGTON|WEST\\s+VIRGINIA|WISCONSIN|WYOMING)(?:[,\\s]+\\d{5}(?:-\\d{4})?)?(?:[,\\s]+(?:USA|UNITED\\s+STATES))?\\s*$");
6868
private static final Pattern ZIP_SUFFIX = Pattern.compile("\\b\\d{5}(?:-\\d{4})?\\s*$");
69-
private static final Pattern STATE_ONLY_LINE = Pattern.compile("(?i)^\\s*[A-Z]{2}(?:\\s+\\d{5}(?:-\\d{4})?)?\\s*$");
7069
private static final Pattern UNIT_LINE = Pattern.compile("(?i)^" + AddressExtractor.UNIT_LABELS + "\\s*#?\\s*[\\p{L}\\p{N}-]*\\s*$");
7170
private static final Pattern UNIT_MARKER_END = Pattern.compile("(?i)(?:^|\\s)" + AddressExtractor.UNIT_LABELS + "\\s*$");
7271
private static final Pattern UNIT_VALUE = Pattern.compile("(?i)^#?[\\p{L}\\p{N}][\\p{L}\\p{N}-]*$");
@@ -191,9 +190,22 @@ public List<PlaceCandidate> extractAddressCandidates(byte[] imageBytes, String n
191190
for (int previousLineIndex = lineIndex - 1; previousLineIndex >= 0; previousLineIndex--) {
192191
OcrLine previous = lines.get(previousLineIndex);
193192
String previousText = previous.text().trim();
194-
if (!linesAreVerticallyAdjacentOrOverlapping(previous, line) || previousText.length() > 3
193+
boolean verticalOverlap = previous.bottom() > line.top();
194+
boolean horizontallyNear = linesAreHorizontallyNear(previous, line);
195+
boolean mixedAlphaNumericUnit = UNIT_VALUE.matcher(previousText).matches()
196+
&& previousText.chars().anyMatch(Character::isLetter)
197+
&& previousText.chars().anyMatch(Character::isDigit);
198+
boolean numericUnitAfterMarker = UNIT_MARKER_END.matcher(addressText).find()
199+
&& UNIT_VALUE.matcher(previousText).matches();
200+
boolean adjacentNearbyUnit = !verticalOverlap
201+
&& linesAreAdjacent(previous, line)
202+
&& horizontallyNear
203+
&& previousText.length() <= 32
204+
&& (mixedAlphaNumericUnit || numericUnitAfterMarker);
205+
if ((!verticalOverlap && !adjacentNearbyUnit)
206+
|| (previousText.length() > 3 && !adjacentNearbyUnit)
195207
|| addressCandidateText(previousText) != null || isStateOnlyLine(previousText)) break;
196-
if (linesAreHorizontallyNear(previous, line)) {
208+
if (horizontallyNear) {
197209
nearbyFragmentRequiresReview = true;
198210
evidenceLineIndexes.add(previousLineIndex);
199211
}
@@ -637,8 +649,7 @@ private static String adjacentLocality(OcrLine addressLine, OcrLine followingLin
637649
}
638650

639651
private static boolean isStateOnlyLine(String text) {
640-
return text != null && (STATE_ONLY_LINE.matcher(text.trim()).matches()
641-
|| AddressExtractor.isStateOnlyLine(text));
652+
return text != null && AddressExtractor.isStateOnlyLine(text);
642653
}
643654

644655
static String addressCandidateText(String text) {

‎src/test/java/com/jtdev/routelisttotesla/service/AddressOcrServiceTest.java‎

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -193,25 +193,98 @@ void retainsAdjacentNonOverlappingNearbyUnitFragmentAsReviewEvidence() throws Ex
193193
}
194194

195195
@Test
196-
void retainsAdjacentNonOverlappingNearbyUnitFragmentBeforeAddressAsReviewEvidence() throws Exception {
196+
void excludesAdjacentNonOverlappingHeaderFragmentsFromAddressEvidence() throws Exception {
197197
String tsv = TSV_HEADER + "\n" + String.join("\n",
198-
"5\t1\t1\t1\t1\t1\t410\t70\t20\t10\t90\t3B",
199-
"5\t1\t1\t1\t2\t1\t100\t100\t300\t20\t90\t123 MAIN ST",
200-
"5\t1\t1\t1\t3\t1\t100\t140\t120\t20\t90\tDETROIT");
198+
"5\t1\t1\t1\t1\t1\t100\t70\t40\t10\t90\teG»",
199+
"5\t1\t1\t1\t2\t1\t100\t82\t30\t10\t90\t929",
200+
"5\t1\t1\t1\t3\t1\t100\t100\t300\t20\t90\t123 MAIN ST",
201+
"5\t1\t1\t1\t4\t1\t100\t140\t120\t20\t90\tDETROIT");
201202
Map<String, Object> lineEvidence = Map.of(
202-
"1", evidence(1, true, List.of(reading("easyocr", "3B"))),
203-
"2", consensusEvidence("123 MAIN ST"),
204-
"3", consensusEvidence("DETROIT"));
203+
"1", evidence(1, true, List.of(reading("tesseract", "eG»"))),
204+
"2", evidence(1, true, List.of(reading("tesseract", "929"))),
205+
"3", consensusEvidence("123 MAIN ST"),
206+
"4", consensusEvidence("DETROIT"));
205207
String output = consensusOutput(tsv, lineEvidence);
206208
AddressOcrService service = new AddressOcrService("ocr-binary", "eng",
207209
ignored -> new AddressOcrService.ProcessResult(0, output, ""), false);
208210

209211
PlaceCandidate candidate = service.extractAddressCandidates(png(), "route.png", "").get(0);
210212

211213
assertEquals("123 MAIN ST, DETROIT", candidate.text());
214+
assertEquals(3, candidate.ocrAgreement());
215+
assertFalse(candidate.ocrReviewRequired());
216+
assertTrue(candidate.ocrAlternatives().stream().noneMatch(value -> value.startsWith("line 1 ")
217+
|| value.startsWith("line 2 ")));
218+
}
219+
220+
@Test
221+
void retainsAdjacentNonOverlappingNearbyUnitFragmentBeforeAddressAsReviewEvidence() throws Exception {
222+
for (String unit : List.of("3B", "A208")) {
223+
String tsv = TSV_HEADER + "\n" + String.join("\n",
224+
"5\t1\t1\t1\t1\t1\t410\t70\t20\t10\t90\t" + unit,
225+
"5\t1\t1\t1\t2\t1\t100\t100\t300\t20\t90\t123 MAIN ST",
226+
"5\t1\t1\t1\t3\t1\t100\t140\t120\t20\t90\tDETROIT");
227+
Map<String, Object> lineEvidence = Map.of(
228+
"1", evidence(1, true, List.of(reading("easyocr", unit))),
229+
"2", consensusEvidence("123 MAIN ST"),
230+
"3", consensusEvidence("DETROIT"));
231+
String output = consensusOutput(tsv, lineEvidence);
232+
AddressOcrService service = new AddressOcrService("ocr-binary", "eng",
233+
ignored -> new AddressOcrService.ProcessResult(0, output, ""), false);
234+
235+
PlaceCandidate candidate = service.extractAddressCandidates(png(), "route.png", "").get(0);
236+
237+
assertEquals("123 MAIN ST, DETROIT", candidate.text());
238+
assertEquals(1, candidate.ocrAgreement());
239+
assertTrue(candidate.ocrReviewRequired());
240+
assertTrue(candidate.ocrAlternatives().contains("line 1 easyocr: " + unit));
241+
}
242+
}
243+
244+
@Test
245+
void keepsAdjacentPlainNumericFragmentOnlyWhenAddressHasUnitMarker() throws Exception {
246+
for (String unit : List.of("929", "2018")) {
247+
String tsv = TSV_HEADER + "\n" + String.join("\n",
248+
"5\t1\t1\t1\t1\t1\t100\t82\t30\t10\t90\t" + unit,
249+
"5\t1\t1\t1\t2\t1\t100\t100\t300\t20\t90\t123 MAIN ST APT",
250+
"5\t1\t1\t1\t3\t1\t100\t140\t120\t20\t90\tDETROIT, MI");
251+
Map<String, Object> lineEvidence = Map.of(
252+
"1", evidence(1, true, List.of(reading("tesseract", unit))),
253+
"2", consensusEvidence("123 MAIN ST APT"),
254+
"3", consensusEvidence("DETROIT, MI"));
255+
String output = consensusOutput(tsv, lineEvidence);
256+
AddressOcrService service = new AddressOcrService("ocr-binary", "eng",
257+
ignored -> new AddressOcrService.ProcessResult(0, output, ""), false);
258+
259+
PlaceCandidate candidate = service.extractAddressCandidates(png(), "route.png", "").get(0);
260+
261+
assertEquals("123 MAIN ST APT, DETROIT, MI", candidate.text());
262+
assertEquals(1, candidate.ocrAgreement());
263+
assertTrue(candidate.ocrReviewRequired());
264+
assertTrue(candidate.ocrAlternatives().contains("line 1 tesseract: " + unit));
265+
}
266+
}
267+
268+
@Test
269+
void retainsUnknownAdjacentTwoLetterFragmentSoFollowingLocalityStillJoins() throws Exception {
270+
String tsv = TSV_HEADER + "\n" + String.join("\n",
271+
"5\t1\t1\t1\t1\t1\t100\t100\t300\t20\t90\t22 1/2 EASTRIDGE CT",
272+
"5\t1\t1\t1\t2\t1\t100\t124\t30\t10\t90\tNN",
273+
"5\t1\t1\t1\t3\t1\t100\t140\t120\t20\t90\tALBANY NY");
274+
Map<String, Object> lineEvidence = Map.of(
275+
"1", consensusEvidence("22 1/2 EASTRIDGE CT"),
276+
"2", evidence(1, true, List.of(reading("tesseract", "NN"))),
277+
"3", consensusEvidence("ALBANY NY"));
278+
String output = consensusOutput(tsv, lineEvidence);
279+
AddressOcrService service = new AddressOcrService("ocr-binary", "eng",
280+
ignored -> new AddressOcrService.ProcessResult(0, output, ""), false);
281+
282+
PlaceCandidate candidate = service.extractAddressCandidates(png(), "route.png", "").get(0);
283+
284+
assertEquals("22 1/2 EASTRIDGE CT, ALBANY NY", candidate.text());
212285
assertEquals(1, candidate.ocrAgreement());
213286
assertTrue(candidate.ocrReviewRequired());
214-
assertTrue(candidate.ocrAlternatives().contains("line 1 easyocr: 3B"));
287+
assertTrue(candidate.ocrAlternatives().contains("line 2 tesseract: NN"));
215288
}
216289

217290
@Test

0 commit comments

Comments
 (0)