← 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();
  }
})();