Skip to content

Commit 01ae9d5

Browse files
authored
Merge pull request #41 from googlemaps-samples/feat/demo-autocomplete-region-search
fix(demo): implement Place Autocomplete for Region Search
2 parents 2329966 + ff7e931 commit 01ae9d5

6 files changed

Lines changed: 173 additions & 202 deletions

File tree

places_insights/places-insights-demo/help.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,13 @@ function generateGuideHtml() {
9494
9595
<h3>C. Region Search</h3>
9696
<p>
97-
This powerful mode allows you to search by administrative names like cities, states, or postal codes instead of drawing on the map.
97+
This powerful mode allows you to search by administrative regions like cities, states, or postal codes using Place Autocomplete.
9898
</p>
9999
<ol>
100100
<li>Select <strong>Region Search</strong> from the "Demo Type" dropdown.</li>
101-
<li>Choose a <strong>Region Type</strong> from the dropdown. This list is dynamically populated based on the selected ${locationTypeLower}.</li>
102-
<li>Enter a name in the <strong>Region Name(s)</strong> input box (e.g., "London").</li>
103-
<li><strong>(Optional) Add Multiple Regions:</strong> To search across several regions at once, click the <code>+</code> button after typing each name.</li>
101+
<li>Start typing a region name in the <strong>Region</strong> input box (e.g., "Manhattan").</li>
102+
<li>Select the correct region from the dropdown suggestions. This guarantees accurate results by using the exact Place ID.</li>
103+
<li><strong>(Optional) Add Multiple Regions:</strong> To search across several regions at once, continue searching and adding more places to the list.</li>
104104
</ol>
105105
106106
<h3>D. Route Search</h3>
@@ -134,7 +134,7 @@ function generateGuideHtml() {
134134
<li><strong>Business Status:</strong> Filter places by their operational status (Operational, Closed Temporarily, Closed Permanently, or Any). Default is <strong>Operational</strong>.</li>
135135
<li><strong>Attribute Filters:</strong> Filter by Price Level, set min/max ratings, or select checkboxes for amenities (e.g., "Offers Delivery").</li>
136136
<li><strong>Opening Hours:</strong> Select a <strong>Day of Week</strong> and time window (Not available in H3 Function mode).</li>
137-
<li><strong>Brand Filters (US Only):</strong> Filter by Brand Category or Brand Name (Not available in H3 Function mode).</li>
137+
<li><strong>Brand Filters (US Only):</strong> Filter by Brand Name (Not available in H3 Function mode).</li>
138138
</ul>
139139
140140
<h2>5. Choosing Your Visualization</h2>

places_insights/places-insights-demo/index.html

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,19 +92,10 @@ <h1>Places Insights</h1>
9292
</div>
9393

9494
<div id="region-search-controls" class="hidden">
95-
<p class="info-text">Select a region type and enter name(s).</p>
95+
<p class="info-text">Search for a region (e.g., city, state, zip code).</p>
9696
<div class="control-group">
97-
<label for="region-type-select">Region Type</label>
98-
<select id="region-type-select">
99-
<option value="" disabled selected>-- Select location first --</option>
100-
</select>
101-
</div>
102-
<div class="control-group">
103-
<label for="region-name-input">Region Name(s)</label>
104-
<div class="input-with-button">
105-
<input type="text" id="region-name-input" placeholder="e.g., London, California...">
106-
<button id="add-region-btn" class="secondary-button">+</button>
107-
</div>
97+
<label for="region-autocomplete-container">Region</label>
98+
<div id="region-autocomplete-container"></div>
10899
</div>
109100
<ul id="selected-regions-list"></ul>
110101
</div>

places_insights/places-insights-demo/main.js

Lines changed: 49 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,12 @@ function handleStartDemo() {
7272
const brandFilters = document.getElementById('brand-filters');
7373
// Logic for Brand Filters visibility
7474
let isUS = false;
75+
let countryCode;
7576
if (DATASET === 'SAMPLE') {
76-
isUS = SAMPLE_LOCATIONS[selectedCountryName] === 'us';
77+
countryCode = SAMPLE_LOCATIONS[selectedCountryName];
78+
isUS = countryCode === 'us';
7779
} else {
80+
countryCode = COUNTRY_CODES[selectedCountryName];
7881
isUS = selectedCountryName === 'United States';
7982
}
8083

@@ -84,7 +87,11 @@ function handleStartDemo() {
8487
brandFilters.classList.add('hidden');
8588
}
8689

87-
populateRegionTypes(selectedCountryName);
90+
// Restrict Region Autocomplete to the active country
91+
if (window.regionAutocomplete) {
92+
window.regionAutocomplete.includedRegionCodes = [countryCode];
93+
}
94+
8895
startDemo(selectedCountryName);
8996
} else {
9097
alert("Please select a location to begin.");
@@ -102,22 +109,6 @@ function handleChangeCountryClick() {
102109
invalidateQueryState();
103110
}
104111

105-
/**
106-
* Handles clicks on the "+" button to add a region to the list.
107-
*/
108-
function handleAddRegionClick() {
109-
const regionInput = document.getElementById('region-name-input');
110-
const regionList = document.getElementById('selected-regions-list');
111-
const regionName = regionInput.value.trim();
112-
113-
if (regionName) {
114-
addTag(toTitleCase(regionName), regionList); // Apply Title Case here
115-
regionInput.value = '';
116-
regionInput.focus();
117-
invalidateQueryState();
118-
}
119-
}
120-
121112
/**
122113
* Handles clicks on the "+" button to add a brand to the list.
123114
*/
@@ -192,41 +183,49 @@ function handleCopyQueryClick() {
192183

193184

194185
/**
195-
* Populates the Region Type dropdown based on the selected country's configuration.
186+
* Initializes the Place Autocomplete (New) web components for Region search.
196187
*/
197-
function populateRegionTypes(locationName) {
198-
const select = document.getElementById('region-type-select');
199-
select.innerHTML = '';
200-
201-
let countryCode;
202-
if (DATASET === 'SAMPLE') {
203-
countryCode = SAMPLE_LOCATIONS[locationName];
204-
} else {
205-
countryCode = COUNTRY_CODES[locationName];
206-
}
188+
async function initializeRegionSearch() {
189+
const { PlaceAutocompleteElement } = await google.maps.importLibrary("places");
190+
const container = document.getElementById('region-autocomplete-container');
207191

208-
const regionFields = REGION_FIELD_CONFIG[countryCode];
192+
const autocomplete = new PlaceAutocompleteElement();
193+
container.appendChild(autocomplete);
209194

210-
if (regionFields && regionFields.length > 0) {
211-
const defaultOption = document.createElement('option');
212-
defaultOption.value = '';
213-
defaultOption.textContent = '-- Select a region type --';
214-
defaultOption.disabled = true;
215-
defaultOption.selected = true;
216-
select.appendChild(defaultOption);
195+
autocomplete.addEventListener('gmp-select', async ({ placePrediction }) => {
196+
if (!placePrediction) return;
197+
invalidateQueryState();
217198

218-
regionFields.forEach(field => {
219-
const option = document.createElement('option');
220-
option.value = `${field.field}|${field.type}`;
221-
option.textContent = field.label;
222-
select.appendChild(option);
223-
});
224-
} else {
225-
const disabledOption = document.createElement('option');
226-
disabledOption.textContent = 'Region search not available';
227-
disabledOption.disabled = true;
228-
select.appendChild(disabledOption);
229-
}
199+
const place = placePrediction.toPlace();
200+
// Request the viewport field to properly frame the region on the map
201+
await place.fetchFields({ fields: ['id', 'displayName', 'types', 'location', 'viewport'] });
202+
203+
let targetColumn = null;
204+
if (place.types) {
205+
for (const type of place.types) {
206+
if (REGION_TYPE_TO_BQ_COLUMN[type]) {
207+
targetColumn = REGION_TYPE_TO_BQ_COLUMN[type];
208+
break;
209+
}
210+
}
211+
}
212+
213+
if (!targetColumn) {
214+
alert(`The selected place type is not supported as a search region. Please select a valid city, state, or neighborhood.`);
215+
autocomplete.inputValue = '';
216+
return;
217+
}
218+
219+
if (place.location || place.viewport) {
220+
addRegionTag(place.displayName, place.id, targetColumn, place.location, place.viewport);
221+
} else {
222+
alert("Could not retrieve location for this place.");
223+
}
224+
225+
autocomplete.inputValue = '';
226+
});
227+
228+
window.regionAutocomplete = autocomplete;
230229
}
231230

232231
/**
@@ -296,7 +295,6 @@ window.onload = () => {
296295
demoTypeSelect: document.getElementById('demo-type-select'),
297296
startButton: document.getElementById("start-demo-btn"),
298297
changeCountryBtn: document.getElementById('change-country-btn'),
299-
addRegionBtn: document.getElementById('add-region-btn'),
300298
addBrandBtn: document.getElementById('add-brand-btn'),
301299
showHelpBtn: document.getElementById('show-help-btn'),
302300
guideModal: document.getElementById('guide-modal'),
@@ -325,7 +323,6 @@ window.onload = () => {
325323
elements.copyQueryBtn.addEventListener('click', handleCopyQueryClick);
326324
elements.startButton.addEventListener("click", handleStartDemo);
327325
elements.changeCountryBtn.addEventListener('click', handleChangeCountryClick);
328-
elements.addRegionBtn.addEventListener('click', handleAddRegionClick);
329326
elements.addBrandBtn.addEventListener('click', handleAddBrandClick);
330327
elements.showHelpBtn.addEventListener('click', showHelpModal);
331328
elements.closeHelpBtn.addEventListener('click', () => hideModal('guide-modal'));
@@ -362,6 +359,7 @@ window.onload = () => {
362359

363360
initializeIdentityServices();
364361
initializeAutocomplete(document.getElementById('place-type-input'));
362+
initializeRegionSearch();
365363
initializeRouteSearch();
366364
initializeAccordion();
367365

places_insights/places-insights-demo/query.js

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -38,36 +38,55 @@ function getPolygonSearchParams() {
3838
}
3939

4040
async function getRegionSearchParams() {
41-
const regionInputValue = document.getElementById('region-type-select').value;
42-
const regionNameInput = toTitleCase(document.getElementById('region-name-input').value.trim());
43-
const regionTags = [...document.querySelectorAll('#selected-regions-list span')].map(s => s.textContent);
41+
const regionTags = [...document.querySelectorAll('#selected-regions-list .selected-region-tag')];
4442

45-
if (!regionInputValue || (!regionNameInput && regionTags.length === 0)) {
46-
return { success: false, message: "Select a Region Type and enter at least one Region Name." };
43+
if (regionTags.length === 0) {
44+
return { success: false, message: "Search for and select at least one Region." };
4745
}
4846

49-
const uniqueRegionNames = [...new Set([...regionTags, regionNameInput].filter(Boolean))];
50-
const [field, dataType] = regionInputValue.split('|');
51-
const filter = buildRegionFilter(field, dataType, uniqueRegionNames);
47+
// Group tags by their target column and type to build the filter
48+
const columnsToIds = {};
49+
regionTags.forEach(tag => {
50+
const col = tag.dataset.column;
51+
const colType = tag.dataset.colType;
52+
if (!columnsToIds[col]) columnsToIds[col] = { type: colType, ids: [] };
53+
columnsToIds[col].ids.push(tag.dataset.id);
54+
});
55+
56+
// Build exact match filters using the Place IDs based on their column data type
57+
const filterParts = [];
58+
for (const [col, data] of Object.entries(columnsToIds)) {
59+
const idList = data.ids.map(id => `'${id}'`).join(', ');
60+
if (data.type === 'STRING') {
61+
filterParts.push(`places.${col} IN (${idList})`);
62+
} else {
63+
filterParts.push(`EXISTS (SELECT 1 FROM UNNEST(places.${col}) AS id WHERE id IN (${idList}))`);
64+
}
65+
}
66+
const filter = `(${filterParts.join(' OR ')})`;
5267

53-
updateStatus('Geocoding regions...');
54-
const { Geocoder } = await google.maps.importLibrary("geocoding");
68+
// Calculate map bounds from tags, prioritizing the viewport for accurate framing
5569
const bounds = new google.maps.LatLngBounds();
56-
const geocodePromises = uniqueRegionNames.map(name => new Geocoder().geocode({ address: `${name}, ${selectedCountryName}` }));
57-
58-
const resultsArray = await Promise.all(geocodePromises);
59-
resultsArray.forEach(({ results }) => {
60-
if (results.length > 0) bounds.union(results[0].geometry.viewport);
70+
let hasBounds = false;
71+
72+
regionTags.forEach(tag => {
73+
if (tag.dataset.north) {
74+
const sw = { lat: parseFloat(tag.dataset.south), lng: parseFloat(tag.dataset.west) };
75+
const ne = { lat: parseFloat(tag.dataset.north), lng: parseFloat(tag.dataset.east) };
76+
bounds.union(new google.maps.LatLngBounds(sw, ne));
77+
hasBounds = true;
78+
} else if (tag.dataset.lat) {
79+
bounds.extend({ lat: parseFloat(tag.dataset.lat), lng: parseFloat(tag.dataset.lng) });
80+
hasBounds = true;
81+
}
6182
});
62-
63-
if (bounds.isEmpty()) {
64-
throw new Error(`Could not geocode any of the specified regions.`);
83+
84+
if (hasBounds) {
85+
map.fitBounds(bounds);
6586
}
66-
map.fitBounds(bounds);
6787

6888
return {
69-
success: true, filter, center: bounds.getCenter(), searchAreaVar: '',
70-
isMultiRegion: uniqueRegionNames.length > 1, regionType: field, regionDataType: dataType
89+
success: true, filter, center: bounds.getCenter(), searchAreaVar: ''
7190
};
7291
}
7392

@@ -199,10 +218,12 @@ async function runQuery() {
199218

200219
// Brand Filters (only applicable here, not in H3 Function)
201220
const brandNames = [...document.querySelectorAll('#selected-brands-list span')].map(s => s.textContent);
221+
const pendingBrandInput = document.getElementById('brand-name-input').value.trim();
222+
if (pendingBrandInput && !brandNames.includes(pendingBrandInput)) {
223+
brandNames.push(pendingBrandInput);
224+
}
202225
if (brandNames.length > 0) allFilters.push(buildBrandFilter(brandNames));
203226

204-
// Brand Category is removed as we no longer have brands.json data
205-
206227
const openingDay = document.getElementById('day-of-week-select').value;
207228
const hoursFilter = buildOpeningHoursFilter(openingDay, document.getElementById('start-time-input').value, document.getElementById('end-time-input').value);
208229
if (hoursFilter.whereClause) allFilters.push(hoursFilter.whereClause);
@@ -241,7 +262,7 @@ async function runQuery() {
241262
const h3Res = parseInt(document.getElementById('h3-resolution-slider').value, 10);
242263
sqlQuery = buildH3DensityQuery(searchParams.searchAreaVar, fromClause, whereClause, h3Res);
243264
} else {
244-
sqlQuery = buildAggregateQuery(searchParams.searchAreaVar, fromClause, whereClause, placeTypes, isBrandQuery, searchParams.isMultiRegion, searchParams.regionType, searchParams.regionDataType);
265+
sqlQuery = buildAggregateQuery(searchParams.searchAreaVar, fromClause, whereClause, placeTypes, isBrandQuery);
245266
}
246267
lastExecutedQuery = sqlQuery;
247268

@@ -292,8 +313,6 @@ function buildBrandFilter(brandNames) {
292313
return `brands.name IN (${sanitizedNames})`;
293314
}
294315

295-
// Brand Category Filter removed
296-
297316
function buildOpeningHoursFilter(day, startTime, endTime) {
298317
if (!day || (!startTime && !endTime)) return { unnestClause: '', whereClause: '' };
299318
const unnestClause = `, UNNEST(places.regular_opening_hours.${day}) AS opening_period`;
@@ -317,21 +336,9 @@ function buildRatingFilter(min, max) {
317336
return '';
318337
}
319338

320-
function buildRegionFilter(regionType, regionDataType, regionNames) {
321-
if (!regionType || !regionNames || regionNames.length === 0) return '';
322-
const sanitizedNames = regionNames.map(name => `'${name.replace(/'/g, "\\'")}'`).join(', ');
323-
if (regionDataType === 'STRING') {
324-
return `places.${regionType} IN (${sanitizedNames})`;
325-
}
326-
return `EXISTS (SELECT 1 FROM UNNEST(places.${regionType}) AS name WHERE name IN (${sanitizedNames}))`;
327-
}
328-
329339
// --- UNIFIED QUERY BUILDERS ---
330340

331-
function buildAggregateQuery(searchAreaVar, fromClause, whereClause, types, isBrandQuery, isMultiRegion, regionType, regionDataType) {
332-
if (isMultiRegion && regionDataType === 'STRING') {
333-
return `${searchAreaVar} SELECT WITH AGGREGATION_THRESHOLD places.${regionType} AS region_name, COUNT(*) AS count ${fromClause} ${whereClause} GROUP BY region_name ORDER BY count DESC`;
334-
}
341+
function buildAggregateQuery(searchAreaVar, fromClause, whereClause, types, isBrandQuery) {
335342
if (isBrandQuery) {
336343
return `${searchAreaVar} SELECT WITH AGGREGATION_THRESHOLD brands.name, COUNT(places.id) AS count ${fromClause} ${whereClause} GROUP BY brands.name ORDER BY count DESC`;
337344
}

0 commit comments

Comments
 (0)