← back to Costa Rica
scripts/ingest/google-places.js
105 lines
'use strict';
// Google Places Nearby Search — sweep each region centroid for businesses.
// Reads GOOGLE_PLACES_API_KEY from env; if missing, exits with notes.
const { fetchJson, slug, sleep, regionMap, upsertPlace, startRun, finishRun, pool } = require('./_lib');
const KEY = process.env.GOOGLE_PLACES_API_KEY;
const RADIUS_M = 5000;
const TYPE_TO_VERTICAL = {
lodging: ['tourism', 'tourism_hotel'],
restaurant: ['tourism', 'tourism_restaurant'],
cafe: ['service', 'service_food'],
bakery: ['service', 'service_food'],
bar: ['service', 'service_food'],
meal_takeaway: ['service', 'service_food'],
spa: ['service', 'service_beauty'],
beauty_salon: ['service', 'service_beauty'],
hair_care: ['service', 'service_beauty'],
gym: ['service', 'service_fitness'],
pet_store: ['service', 'service_pet'],
veterinary_care: ['service', 'service_pet'],
car_repair: ['service', 'service_auto'],
car_wash: ['service', 'service_auto'],
car_rental: ['tourism', 'tourism_tour'],
travel_agency: ['tourism', 'tourism_tour'],
tourist_attraction:['tourism', 'tourism_tour'],
shopping_mall: ['service', 'service_retail'],
store: ['service', 'service_retail'],
real_estate_agency:['rentals', 'rentals_realestate'],
};
const TYPES = Object.keys(TYPE_TO_VERTICAL);
async function nearby(lat, lng, type, pageToken = null) {
const url = new URL('https://maps.googleapis.com/maps/api/place/nearbysearch/json');
url.searchParams.set('location', `${lat},${lng}`);
url.searchParams.set('radius', RADIUS_M);
url.searchParams.set('type', type);
url.searchParams.set('key', KEY);
if (pageToken) url.searchParams.set('pagetoken', pageToken);
return await fetchJson(url.toString());
}
(async () => {
if (!KEY) { console.warn('[google-places] GOOGLE_PLACES_API_KEY missing — skipping'); process.exit(0); }
const runId = await startRun('google-places', 'Sweep CR regions via Places Nearby Search');
const rmap = await regionMap();
let total = 0, added = 0, updated = 0, errors = 0;
try {
const targets = rmap.rows.filter(r => r.lat && r.lng && r.region_type !== 'region');
for (const region of targets) {
for (const type of TYPES) {
try {
let pageToken = null, pages = 0;
do {
const res = await nearby(region.lat, region.lng, type, pageToken);
if (res.status !== 'OK' && res.status !== 'ZERO_RESULTS') {
console.warn(`[google-places] ${region.slug}/${type} status=${res.status}`);
break;
}
const [category, vertical] = TYPE_TO_VERTICAL[type];
for (const p of (res.results || [])) {
const name = p.name; if (!name) continue;
const placeSlug = slug(`gp-${p.place_id}`);
const r = await upsertPlace({
slug: placeSlug,
name,
category, vertical,
region_id: region.id,
description: `${vertical.replace(/_/g,' ')} in ${region.name}.`,
address: p.vicinity || null,
lat: p.geometry?.location?.lat ?? null,
lng: p.geometry?.location?.lng ?? null,
rating: p.rating ?? null,
source: 'google-places',
source_url: `https://www.google.com/maps/place/?q=place_id:${p.place_id}`,
});
if (r.inserted) added++; else updated++;
total++;
}
pageToken = res.next_page_token || null;
pages++;
if (pageToken) await sleep(2200); // Google needs ~2s before next_page_token is valid
} while (pageToken && pages < 3);
} catch (e) {
errors++;
console.warn(`[google-places] ${region.slug}/${type} err:`, e.message);
}
await sleep(120);
}
}
await finishRun(runId, { rows_in: total, rows_added: added, rows_updated: updated, status: errors ? 'partial' : 'ok', notes: `errors=${errors}` });
console.log(`[google-places] in=${total} added=${added} updated=${updated} errors=${errors}`);
} catch (e) {
await finishRun(runId, { status: 'error', notes: e.message });
console.error('[google-places] FAIL', e.message);
process.exitCode = 1;
} finally {
await pool.end();
}
})();