Skip to content

Commit 901eff2

Browse files
committed
fix: corroborate address recovery and isolate OCR metadata
1 parent c9693e5 commit 901eff2

10 files changed

Lines changed: 839 additions & 38 deletions

File tree

‎docs/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Documentation
22

3+
- [General address reliability (2026-10-19)](general-address-reliability-2026-10-19.md): bounded provider-corroborated recovery, OCR layout boundaries and release checks.
4+
35
- [Apartment lookup and OCR evidence (2026-10-18)](southfield-address-and-ocr-evidence-2026-10-18.md): provider unit-label normalization, spatial metadata exclusion and saved-location migration.
46

57
- [Street suffix and township recovery (2026-10-17)](address-aliases-2026-10-17.md): deployed comprehensive USPS catalog, supplied HEIC diagnosis, final native qualification and release proof.

‎docs/development-reference.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ TAP-managed sign-in and stable-subject authorization replace independent Google
44

55
## Source flow
66

7+
### October 19 provider-corroborated recovery
8+
9+
Current extraction is `ocr-consensus-v9`; geocoding is `street-address-v7`.
10+
Spatial evidence also excludes vertically adjacent side counters while retaining
11+
nearby ambiguous unit fragments. Geocoding first applies strict matching, then
12+
can corroborate a unique precise candidate through reverse locality verification
13+
or constrained-original plus canonical street queries. No address-specific
14+
replacement mappings are used. House numbers, street-name letters, directions,
15+
unit conflicts, state/country, partial/city results and ambiguity remain guarded.
16+
Recovered results expose Google's map address while retaining input text and
17+
the distinction between a verified unit and a verified building. Saved results
18+
under earlier policies are revalidated. See the
19+
[recovery contract and checks](general-address-reliability-2026-10-19.md).
20+
721
### October 18 spatial OCR evidence
822

923
Extraction version `ocr-consensus-v8` separates short package counters from
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# General address reliability
2+
3+
The October 19 report exposed two geocoding representation differences and a
4+
deployment gap. Production still served the October 17 image, so the spatial
5+
OCR evidence fix pushed on October 18 had not reached the running application.
6+
The supplied original HEIC was converted and visually inspected; under the newer
7+
parser, all six street/unit lines already had three-engine agreement without
8+
package-counter warnings.
9+
10+
Google's precise Hall Road result uses a different postal city. Reverse geocoding
11+
also returns the exact requested house/street/city 1.59 metres away. The Hilltop
12+
result uses Drive where the screenshot says Court; both its constrained original
13+
query and its canonical query return the same rooftop place ID. These are
14+
observations from private diagnostic responses, not address replacement rules.
15+
16+
## General recovery contract
17+
18+
Strict street matching remains the fast path. Recovery can propose only a unique,
19+
non-partial rooftop street-level result with the requested house number, street
20+
name, directions, state and country. The input must include city and state. Only
21+
one semantic dimension may differ:
22+
23+
- If no recognized locality level explicitly corroborates the requested city,
24+
reverse geocoding must return a full matching house/street/locality address
25+
within ten metres. A city, county, postal-code centre or nearby different house
26+
cannot supply that proof. The original rooftop location remains the target.
27+
- For a different street type or compound-name spacing, a query containing the
28+
original house/street/unit and structured locality must return the proposed
29+
same rooftop place ID. A second query containing the canonical street must
30+
corroborate it again, within two metres. A competing same-number/stem destination
31+
in either proof blocks correction, regardless of result order.
32+
33+
Recovery never rewrites house numbers, street-name letters, directions, apartment
34+
identifiers, state or country. It cannot combine city and street corrections.
35+
Missing or conflicting provider components, partial matches, invalid coordinates,
36+
ambiguous destinations and genuine unit conflicts remain guarded. Verification
37+
of a recovered apartment requires matching unit evidence in the proposal and
38+
proofs; otherwise only the building is verified and the apartment is retained.
39+
40+
Proof queries share the existing eight-request stop ceiling, 25-second stop and
41+
90-second batch deadlines, per-owner/global budgets, transient retries and 60 ms
42+
global request pacing. They do not use a separate unlimited retry path. No
43+
per-address lookup table, hosted OCR or OpenAI key is introduced.
44+
45+
## OCR, feedback and saved results
46+
47+
Extraction `ocr-consensus-v9` also handles a far-column short counter that is
48+
vertically adjacent rather than overlapping: it cannot interrupt a wrapped unit
49+
or the street-to-city join. Nearby uncertain unit fragments remain evidence and
50+
still require review. Strict consensus packet validation and all three engines
51+
remain intact.
52+
53+
Geocoding `street-address-v7` invalidates older location metadata in cache/session
54+
validation. Recovered rows show Google's map address and retain the original
55+
address text. Unit verification and building-only verification remain distinct;
56+
the normal retry/loading/success feedback and uncertain-command holds remain.
57+
58+
## Validation and release
59+
60+
- Clean package: 228 Java tests passed, including 36 name/state/unit combinations
61+
and rejection/proof-budget tests. All 36 frontend tests passed, including v5/v6
62+
saved-session migration and safe rendering of provider text.
63+
- Eleven user-authorized Google diagnostics were recorded privately. Replaying
64+
those bodies through the actual geocoder verifies all six supplied rows in
65+
order while preserving address text: nine fixture requests, no external calls
66+
or vehicle commands. Hall uses two requests; Hilltop uses three.
67+
- Native baseline on the original HEIC and the prior Southfield screenshot:
68+
eight exact rows, 3/3 address agreement, no false review warnings, 36 Python
69+
tests passed, and disposable QA app/fixtures cleaned up. This baseline used
70+
source overlays and is not final-image qualification.
71+
72+
Final image qualification and deployment evidence will be recorded after the
73+
source CI, security/storage/publication gates and immutable image checks finish.
74+
No vehicle commands are part of validation. Credentials, original delivery
75+
screenshots and provider bodies remain outside source control. Existing data,
76+
TAP authorization, CSRF, ordering/dedupe and eight-stop grouping are preserved.
77+
78+
API references: Google's [forward geocoding response contract](https://developers.google.com/maps/documentation/geocoding/guides-v3/requests-geocoding)
79+
and [reverse-geocoding contract](https://developers.google.com/maps/documentation/geocoding/guides-v3/requests-reverse-geocoding).

‎src/main/java/com/jtdev/routelisttotesla/model/LocationResolution.java‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ public record LocationResolution(
77
String reasonCode,
88
int attempts,
99
String policyVersion,
10-
Integer retryAfterSeconds) {
10+
Integer retryAfterSeconds,
11+
String matchedAddress) {
12+
13+
public LocationResolution(Status status, String strategy, String reasonCode, int attempts,
14+
String policyVersion, Integer retryAfterSeconds) {
15+
this(status, strategy, reasonCode, attempts, policyVersion, retryAfterSeconds, null);
16+
}
1117

1218
public enum Status {
1319
VERIFIED_UNIT, VERIFIED_BUILDING, NO_MATCH, AMBIGUOUS, DEFERRED, LOOKUP_UNAVAILABLE

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

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646

4747
@Service
4848
public class AddressOcrService {
49-
public static final String EXTRACTION_VERSION = "ocr-consensus-v8";
49+
public static final String EXTRACTION_VERSION = "ocr-consensus-v9";
5050

5151
static final int MAX_IMAGE_BYTES = 12 * 1024 * 1024;
5252
static final long MAX_IMAGE_PIXELS = 20_000_000L;
@@ -185,15 +185,16 @@ public List<PlaceCandidate> extractAddressCandidates(byte[] imageBytes, String n
185185

186186
int lastLineIndex = lineIndex;
187187
int lastAddressLineIndex = lineIndex;
188-
boolean overlappingFragment = false;
188+
boolean nearbyFragmentRequiresReview = false;
189189
List<Integer> evidenceLineIndexes = new ArrayList<>();
190190
evidenceLineIndexes.add(lineIndex);
191191
for (int previousLineIndex = lineIndex - 1; previousLineIndex >= 0; previousLineIndex--) {
192192
OcrLine previous = lines.get(previousLineIndex);
193-
if (previous.bottom() <= line.top() || previous.text().trim().length() > 3
194-
|| addressCandidateText(previous.text()) != null) break;
193+
String previousText = previous.text().trim();
194+
if (!linesAreVerticallyAdjacentOrOverlapping(previous, line) || previousText.length() > 3
195+
|| addressCandidateText(previousText) != null || isStateOnlyLine(previousText)) break;
195196
if (linesAreHorizontallyNear(previous, line)) {
196-
overlappingFragment = true;
197+
nearbyFragmentRequiresReview = true;
197198
evidenceLineIndexes.add(previousLineIndex);
198199
}
199200
}
@@ -204,16 +205,18 @@ public List<PlaceCandidate> extractAddressCandidates(byte[] imageBytes, String n
204205
String continuation = nextLine.text().trim();
205206
boolean addressContinuation = isAddressContinuation(addressText, continuation);
206207
boolean smallFragment = continuation.length() <= 3
207-
&& addressCandidateText(continuation) == null;
208-
boolean verticalOverlap = nextLine.top() < previousLine.bottom();
209-
boolean smallOverlap = smallFragment && verticalOverlap
210-
&& linesAreHorizontallyNear(previousLine, nextLine);
211-
if (smallFragment && verticalOverlap && !smallOverlap) {
208+
&& addressCandidateText(continuation) == null
209+
&& !isStateOnlyLine(continuation);
210+
boolean verticallyConnected = linesAreVerticallyAdjacentOrOverlapping(previousLine, nextLine);
211+
boolean horizontallyNear = linesAreHorizontallyNear(previousLine, nextLine);
212+
boolean nearbyShortFragment = smallFragment && verticallyConnected && horizontallyNear;
213+
boolean sideColumnShortFragment = smallFragment && verticallyConnected && !horizontallyNear;
214+
if (sideColumnShortFragment) {
212215
// Skip side-column metadata but keep looking from the last address line.
213216
nextLineIndex++;
214217
continue;
215218
}
216-
if (!linesAreAdjacent(previousLine, nextLine) && !smallOverlap) {
219+
if (!linesAreAdjacent(previousLine, nextLine) && !nearbyShortFragment) {
217220
break;
218221
}
219222
if (addressContinuation) {
@@ -222,9 +225,9 @@ public List<PlaceCandidate> extractAddressCandidates(byte[] imageBytes, String n
222225
lastAddressLineIndex = nextLineIndex;
223226
evidenceLineIndexes.add(lastLineIndex);
224227
nextLineIndex++;
225-
} else if (smallOverlap) {
228+
} else if (nearbyShortFragment) {
226229
// A nearby short fragment may be an unlabelled unit, so keep it reviewable.
227-
overlappingFragment = true;
230+
nearbyFragmentRequiresReview = true;
228231
lastLineIndex = nextLineIndex;
229232
lastAddressLineIndex = nextLineIndex;
230233
evidenceLineIndexes.add(lastLineIndex);
@@ -282,7 +285,7 @@ && isStateOnlyLine(stateLine.text())
282285
0,
283286
null,
284287
ocrEvidence.agreement(),
285-
overlappingFragment || ocrEvidence.reviewRequired(),
288+
nearbyFragmentRequiresReview || ocrEvidence.reviewRequired(),
286289
ocrEvidence.alternatives()));
287290
lineIndex = lastLineIndex;
288291
}
@@ -651,8 +654,12 @@ private static boolean linesAreAdjacent(OcrLine first, OcrLine second) {
651654
return gap >= -overlap && gap <= Math.max(12, (int) Math.ceil(lineHeight * 1.5));
652655
}
653656

657+
private static boolean linesAreVerticallyAdjacentOrOverlapping(OcrLine first, OcrLine second) {
658+
return second.top() < first.bottom() || linesAreAdjacent(first, second);
659+
}
660+
654661
private static boolean linesAreHorizontallyNear(OcrLine first, OcrLine second) {
655-
// Badge-column text can overlap an address vertically; short fragments must also sit in its text column.
662+
// Short fragments can be vertically adjacent; require them to sit in the address text column.
656663
int gap = Math.max(0, Math.max(first.left() - second.right(), second.left() - first.right()));
657664
int lineHeight = Math.max(first.bottom() - first.top(), second.bottom() - second.top());
658665
return gap <= Math.max(12, (int) Math.ceil(lineHeight * 1.5));

0 commit comments

Comments
 (0)