Skip to content
2 changes: 1 addition & 1 deletion dist/bpc-web-components.js

Large diffs are not rendered by default.

103 changes: 96 additions & 7 deletions src/address-search/AddressSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ export function AddressSearch({
portalRoot,
}: AddressSearchProps) {
const places = useMapsLibrary("places");
const addressValidation = useMapsLibrary("addressValidation");
const token = useRef<google.maps.places.AutocompleteSessionToken | null>(
null,
);
const placesRef = useRef<
Record<string, google.maps.places.AutocompleteSuggestion | undefined>
>({});
const correctedTextRef = useRef<Record<string, string>>({});
const [inputValue, setInputValue] = useState<string>("");
const searchQuery = inputValue.trim();
const [cache, setCache] = useState<
Expand All @@ -41,7 +43,7 @@ export function AddressSearch({
>([]);

useEffect(() => {
if (!places) return;
if (!places || !addressValidation) return;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Autocomplete completely blocked until addressValidation library finishes loading

At line 46, the guard if (!places || !addressValidation) return; prevents any autocomplete requests from firing until the addressValidation library has loaded. Previously, autocomplete only needed places to be available. Since addressValidation is a separate library loaded asynchronously via useMapsLibrary (src/utils/useMapsLibrary.ts:27-30), this adds latency before any suggestions can appear. Worse, if the addressValidation library fails to load (e.g., not enabled on the API key, network error), autocomplete will be permanently non-functional.

The address validation calls are an enhancement (correcting city names) and should degrade gracefully — the autocomplete suggestions should still be shown even if validation hasn't loaded yet.

Prompt for agents
In src/address-search/AddressSearch.tsx, line 46, change the guard back to only require `places` (not `addressValidation`). The address validation enhancement should be optional/graceful. Specifically:

1. Change line 46 from `if (!places || !addressValidation) return;` to `if (!places) return;`
2. Inside the `.then(async ({ suggestions }) => { ... })` callback (around lines 78-132), wrap the address validation `Promise.all` block in a check: `if (addressValidation) { ... }`. If `addressValidation` is not loaded yet, just skip the validation step and return suggestions immediately.
3. Make sure the `addressValidation` dependency is still in the useEffect deps array (line 138) so that when it loads later, it can trigger re-fetching of validation data for queries that were already cached without validation.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


// Create new token if not exists
if (!token.current) {
Expand All @@ -63,19 +65,80 @@ export function AddressSearch({
// region: "US", // Don't restrict to US -- this changes the way the formatted address is returned
language: "en",
includedPrimaryTypes: ["street_address"],
}).then(({ suggestions }) => {
}).then(async ({ suggestions }) => {
suggestions.forEach((suggestion) => {
if (!suggestion.placePrediction?.placeId) return;
placesRef.current[suggestion.placePrediction.placeId] =
suggestion;
});

// Use Address Validation API to get correct USPS city names.
// Autocomplete secondaryText returns CDPs like "Briarcliff"
// instead of the postal city like "Austin".
await Promise.all(
suggestions.map(async (suggestion) => {
const placeId = suggestion.placePrediction?.placeId;
const streetAddress =
suggestion.placePrediction?.mainText?.text;
if (
!placeId ||
!streetAddress ||
correctedTextRef.current[placeId]
)
return;

// Use whichever has more info: user's input (may include
// city/state) or the autocomplete street address
const addressInput =
searchQuery.length > streetAddress.length
? searchQuery
: streetAddress;
Comment on lines +90 to +95
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we think this is better / more accurate? longer length doesnt represent more information necessarily right?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should always honor / respect user's input.


try {
const validation =
await addressValidation.AddressValidation.fetchAddressValidation(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is gonna get extremely expensive. Rough estimate is probably at least 10x our cost. We should find a better solution here or only fetch validation after selection.

Copy link
Copy Markdown

@ruchir-bpc ruchir-bpc Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, @divazbozz looks like has a better solution. If so, i'll be happy to close this one.

ref:
https://github.com/BasePowerCompany/bpc-web-components/pull/31/changes

{
address: {
addressLines: [addressInput],
regionCode: "US",
},
uspsCASSEnabled: true,
},
);

const uspsCity =
validation.uspsData?.standardizedAddress?.city;
const postalAddress = validation.address?.postalAddress;
const rawCity = uspsCity || postalAddress?.locality || "";
// USPS city is uppercase (e.g. "AUSTIN"), title-case it
const city = rawCity
.toLowerCase()
.replace(/\b\w/g, (c: string) => c.toUpperCase());
const state = postalAddress?.administrativeArea || "";
const country =
postalAddress?.regionCode === "US"
? "USA"
: (postalAddress?.regionCode ?? "");

if (city) {
correctedTextRef.current[placeId] = [city, state, country]
.filter(Boolean)
.join(", ");
}
} catch {
// Fall back to default secondaryText on error
}
}),
);
Comment thread
ruchirx marked this conversation as resolved.

return suggestions;
Comment on lines +68 to 134
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Autocomplete suggestions are blocked until all Address Validation API calls complete

The promise stored in the cache state now includes await Promise.all(...) for N address validation calls (one per suggestion) inside the .then(async ...) handler at lines 67-133. Since .then(async fn) returns a new promise that doesn't resolve until the async function completes, the second useEffect (line 147-150) that calls cached.then(suggestions => setPlacesResult(suggestions)) won't fire until every validation call finishes. Previously, autocomplete results appeared as soon as fetchAutocompleteSuggestions returned. Now they're delayed by the latency of all validation API calls (typically 5 parallel requests), significantly degrading the user experience for type-ahead search.

Prompt for agents
In src/address-search/AddressSearch.tsx, the address validation calls should not block the autocomplete suggestions from appearing. The fix is to separate the address validation from the cached promise so that suggestions display immediately.

One approach: In the .then() callback (lines 67-133), remove the `async` keyword and the `await Promise.all(...)` block from the promise chain. Instead, fire the address validation calls as a side effect (without awaiting them), and when they complete, trigger a re-render so the corrected text is picked up. For example:

1. Change line 67 from `.then(async ({ suggestions }) => {` to `.then(({ suggestions }) => {`
2. Remove the `await` before `Promise.all(...)` on line 77. Instead, use the Promise.all(...).then(() => { /* trigger re-render somehow */ }) pattern.
3. Since correctedTextRef is a ref and doesn't trigger re-renders, you need a mechanism to re-render after validation completes. Options include: (a) store corrected text in state instead of a ref, (b) use a counter state that increments after validation completes to force useMemo recalculation, or (c) call setPlacesResult with the same suggestions again after validation to trigger useMemo.

The simplest approach would be to keep a state-based trigger: add a `const [validationVersion, setValidationVersion] = useState(0)` and increment it when validation calls complete. Include `validationVersion` in the useMemo dependency array at line 201. Then the Promise.all can run in the background and call setValidationVersion(v => v+1) on completion.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this ⏫ , this component is pretty important in terms of how snappy it feels, can we double check latency / user experience (can we show a video or sth to see how it feels)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tried this on npm run dev, and it feels fast and snappy enough.

unclear whats ask here, Do you want me to record a video and upload somewhere as evidence?

}),
};
});
}, [places, searchQuery]);
}, [places, addressValidation, searchQuery]);

useEffect(() => {
let stale = false;
if (!searchQuery) {
setPlacesResult([]);
return;
Expand All @@ -84,9 +147,12 @@ export function AddressSearch({
const cached = cache[searchQuery];
if (cached) {
cached.then((suggestions) => {
setPlacesResult(suggestions);
if (!stale) setPlacesResult(suggestions);
});
}
return () => {
stale = true;
};
}, [cache, searchQuery]);

const handleSelect = useCallback(
Expand All @@ -97,23 +163,43 @@ export function AddressSearch({
setInputValue(
[
place.placePrediction?.mainText?.text,
place.placePrediction?.secondaryText?.text,
correctedTextRef.current[result.id] ||
place.placePrediction?.secondaryText?.text,
]
.filter(Boolean)
.join(", "),
);
// Save corrected city before refs are cleared
const corrected = correctedTextRef.current[result.id];
await place.placePrediction
?.toPlace()
.fetchFields({
fields: ["location", "formattedAddress", "addressComponents"],
})
.then(({ place }) => {
return onSelect?.({ selection: parseAddress(place) });
const selection = parseAddress(place);
// Override city and formattedAddress with USPS-validated city
if (selection && corrected) {
const correctedCity = corrected.split(",")[0].trim();
if (correctedCity) {
const originalCity = selection.address.city;
selection.address.city = correctedCity;
if (originalCity) {
selection.formattedAddress =
selection.formattedAddress.replace(
originalCity,
correctedCity,
);
}
}
Comment on lines +183 to +194
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why only overwrite city? why not use the entire corrected address?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

b/c the only discrepancy I have noticed is with city. Have we seen any other discrepancy / should we prefer if we swap everything?

}
return onSelect?.({ selection });
});

// Clear cached values now that our selection is complete -- the token is only valid until the first toPlace() call
setCache({});
placesRef.current = {};
correctedTextRef.current = {};
token.current = null;
},
[onSelect],
Expand All @@ -124,7 +210,10 @@ export function AddressSearch({
(suggestion) =>
({
mainText: suggestion.placePrediction?.mainText?.text,
secondaryText: suggestion.placePrediction?.secondaryText?.text,
secondaryText:
correctedTextRef.current[
suggestion.placePrediction?.placeId || ""
] || suggestion.placePrediction?.secondaryText?.text,
id: suggestion.placePrediction?.placeId,
}) as Result,
);
Expand Down
1 change: 1 addition & 0 deletions src/utils/useMapsLibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface ApiLibraries {
journeySharing: google.maps.JourneySharingLibrary;
drawing: google.maps.DrawingLibrary;
visualization: google.maps.VisualizationLibrary;
addressValidation: google.maps.AddressValidationLibrary;
}

export const useMapsLibrary = <
Expand Down
Loading