Skip to content

Commit 5676b7e

Browse files
committed
fix: try full unit addresses before geocoding fallback
1 parent 5a1851c commit 5676b7e

4 files changed

Lines changed: 227 additions & 31 deletions

File tree

‎docs/development-reference.md‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,14 @@ Image upload does not automatically dispatch vehicle commands. The reviewed send
172172

173173
## Cache, geocoding, and route state
174174

175-
Geocoding preserves the full reviewed address, including apartment/unit
176-
information, but resolves its primary street address. Broad city/road matches,
177-
partial matches, and mismatched house numbers remain unresolved. The separate
178-
`street-address-v1` geocoding policy version participates in image-cache keys.
175+
Geocoding first looks up the full reviewed address, including apartment/unit
176+
information. If it returns no acceptable destination, one fallback lookup omits
177+
the recognized unit suffix. Both attempts reject broad city/road matches,
178+
partial matches, mismatched house numbers, and invalid coordinates or place IDs.
179+
Stops remain unresolved if neither attempt succeeds; their original text is
180+
preserved. Provider/service errors propagate without address retries. Fallback
181+
calls use the existing pacing and count against owner/global quotas only when
182+
needed. The `street-address-v2` policy version participates in image-cache keys.
179183
Reviewed addresses are resolved again before dispatch so older drafts and
180184
browser-supplied place IDs cannot bypass current validation.
181185

‎docs/geocoding-canfield-2026-09-29.md‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ the selected result's ID, sends it as `placeId`, and TAP prefixes it with
2020

2121
## Remediation
2222

23-
- Preserve apartment/unit information in the reviewed stop, but omit secondary
24-
unit identifiers from the street-address geocoding query.
23+
- Look up the full apartment/unit address first. If no acceptable destination is
24+
returned, retry once without the secondary unit identifier. Preserve the full
25+
address in the reviewed stop regardless of which lookup succeeds.
2526
- Accept only a complete numbered address result; leave broad, partial, or
2627
mismatched results unresolved for review instead of using a city center.
2728
- Include a separate geocoding policy version in image-cache keys. Existing
@@ -37,6 +38,27 @@ No vehicle command was issued during diagnosis or verification.
3738

3839
## Verification
3940

41+
### Apartment-first update (2026-09-30)
42+
43+
The initial fix always removed the unit before lookup. At the user's request,
44+
the current policy tries the full address first and uses unit omission only as
45+
a fallback. It reuses the existing destination validation for both requests.
46+
Successful full-address matches consume no fallback quota. Conditional retries
47+
reserve an additional owner/global call, preserve the admitted batch slot, and
48+
use the existing 60 ms pacing. HTTP, malformed-response, and Google service or
49+
quota errors propagate directly. The cache policy is now `street-address-v2`.
50+
51+
All 133 Java tests passed. Regression coverage verifies full-unit success with
52+
one-call limits, fallback order and pacing, missing/rejected matches, one-retry
53+
limits, unchanged queries without units, provider failures, owner/global quota
54+
enforcement, and duplicate-stop preservation. The revised Java client also
55+
resolved the supplied Canfield address against Google through the fallback;
56+
no route was sent or vehicle command issued. Release evidence is retained in
57+
`output/private/canfield-release-verification.json`, with the initial release
58+
preserved in `output/private/canfield-city-fix-release-verification.json`.
59+
60+
### Initial city-match fix
61+
4062
All 128 Java tests passed, including unit-suffix handling, city/partial/wrong
4163
house-number rejection, stale reviewed-route revalidation, and cache-key
4264
versioning. A live run of the updated Java geocoder against Google resolved

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

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
@Service
3535
public class GeocodingClient {
36-
public static final String GEOCODING_VERSION = "street-address-v1";
36+
public static final String GEOCODING_VERSION = "street-address-v2";
3737

3838
private static final int DEFAULT_OWNER_CALLS_PER_HOUR = 1_000;
3939
private static final int DEFAULT_GLOBAL_CALLS_PER_HOUR = 2_000;
@@ -107,11 +107,11 @@ public List<PlaceCandidate> batchGeocode(String owner, List<PlaceCandidate> inpu
107107
uniqueQueries.putIfAbsent(candidate.text(), candidate);
108108
}
109109

110-
reserve(owner, uniqueQueries.size());
110+
reserve(owner, uniqueQueries.size(), true);
111111
try {
112112
Map<String, PlaceCandidate> resultsByQuery = new HashMap<>();
113113
for (Map.Entry<String, PlaceCandidate> query : uniqueQueries.entrySet()) {
114-
resultsByQuery.put(query.getKey(), geocodeOne(query.getValue()));
114+
resultsByQuery.put(query.getKey(), geocodeOne(owner, query.getValue()));
115115
}
116116
return inputs.stream().map(candidate -> {
117117
PlaceCandidate result = resultsByQuery.get(candidate.text());
@@ -122,7 +122,7 @@ public List<PlaceCandidate> batchGeocode(String owner, List<PlaceCandidate> inpu
122122
}
123123
}
124124

125-
private void reserve(String owner, int calls) {
125+
private void reserve(String owner, int calls, boolean newBatch) {
126126
long now = System.nanoTime();
127127
synchronized (budgetLock) {
128128
globalUsage.prune(now);
@@ -135,14 +135,16 @@ private void reserve(String owner, int calls) {
135135
int activeForOwner = activeOwnerBatches.getOrDefault(owner, 0);
136136
if (calls > ownerCallsPerHour || calls > globalCallsPerHour
137137
|| account.calls + calls > ownerCallsPerHour || globalUsage.calls + calls > globalCallsPerHour
138-
|| activeForOwner >= ownerConcurrentBatches || activeGlobalBatches >= globalConcurrentBatches) {
138+
|| (newBatch && (activeForOwner >= ownerConcurrentBatches || activeGlobalBatches >= globalConcurrentBatches))) {
139139
throw new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS,
140140
"Geocoding rate or concurrency limit exceeded; retry later");
141141
}
142142
account.reserve(now, calls);
143143
globalUsage.reserve(now, calls);
144-
activeOwnerBatches.put(owner, activeForOwner + 1);
145-
activeGlobalBatches++;
144+
if (newBatch) {
145+
activeOwnerBatches.put(owner, activeForOwner + 1);
146+
activeGlobalBatches++;
147+
}
146148
}
147149
}
148150

@@ -182,8 +184,16 @@ private HttpResponse<String> sendWithGlobalPacing(HttpRequest request) {
182184
}
183185
}
184186

185-
private PlaceCandidate geocodeOne(PlaceCandidate candidate) {
186-
URI uri = URI.create(geocodeUrl + "?address=" + encode(withoutSecondaryUnit(candidate.text()))
187+
private PlaceCandidate geocodeOne(String owner, PlaceCandidate candidate) {
188+
PlaceCandidate resolved = lookup(candidate, candidate.text());
189+
String primaryAddress = withoutSecondaryUnit(candidate.text());
190+
if (resolved.pid() != null || primaryAddress.equals(candidate.text())) return resolved;
191+
reserve(owner, 1, false);
192+
return lookup(candidate, primaryAddress);
193+
}
194+
195+
private PlaceCandidate lookup(PlaceCandidate candidate, String address) {
196+
URI uri = URI.create(geocodeUrl + "?address=" + encode(address)
187197
+ "&region=" + encode(regionBias) + "&key=" + encode(apiKey));
188198
HttpRequest request = HttpRequest.newBuilder(uri).timeout(Duration.ofSeconds(15)).GET().build();
189199
HttpResponse<String> response = sendWithGlobalPacing(request);
@@ -196,7 +206,7 @@ private PlaceCandidate geocodeOne(PlaceCandidate candidate) {
196206
}
197207
if (body == null) throw new IllegalStateException("Geocoding returned an empty response");
198208
String status = body.path("status").asString();
199-
if ("OK".equals(status) && body.path("results").isArray() && !body.path("results").isEmpty()) {
209+
if ("OK".equals(status) && body.path("results").isArray()) {
200210
String requestedHouseNumber = houseNumber(candidate.text());
201211
for (JsonNode match : body.path("results")) {
202212
boolean addressType = hasType(match, "street_address") || hasType(match, "premise")
@@ -228,7 +238,9 @@ private PlaceCandidate geocodeOne(PlaceCandidate candidate) {
228238
}
229239

230240
private static String withoutSecondaryUnit(String address) {
231-
return SECONDARY_UNIT.matcher(address).replaceFirst("")
241+
Matcher unit = SECONDARY_UNIT.matcher(address);
242+
if (!unit.find()) return address;
243+
return unit.replaceFirst("")
232244
.replaceAll("\\s+", " ")
233245
.replaceAll("\\s*,\\s*", ", ")
234246
.replaceAll("(?:,\\s*){2,}", ", ")

0 commit comments

Comments
 (0)