← back to Restaurant Directory
scripts/enrich-website-google-places.py
122 lines
#!/usr/bin/env python3
"""Google Places API website enricher for lacountyeats facilities.
For each facility without a website, calls Find Place from Text
(name + city + state) and stores the resulting `website` (and place_id +
formatted_address as a sanity audit trail) into facility_enrichment.
Cost: $0.017 per Find Place call. Free $200/mo credit covers ~11.7k calls.
38k facilities = ~$646 total at sustained rate.
Usage:
GOOGLE_PLACES_API_KEY=... python3 enrich-website-google-places.py --limit 500
python3 enrich-website-google-places.py --limit 500 # reads from ~/Projects/secrets-manager/.env
"""
import argparse, json, os, sys, time, urllib.parse, urllib.request
import psycopg
DB_URL = 'postgresql:///restaurant_professional_directory?host=/tmp'
def load_key():
if os.environ.get('GOOGLE_PLACES_API_KEY'):
return os.environ['GOOGLE_PLACES_API_KEY']
try:
for line in open(os.path.expanduser('~/Projects/secrets-manager/.env')):
if line.startswith('GOOGLE_PLACES_API_KEY='):
return line.split('=', 1)[1].strip().strip('"').strip("'")
except Exception:
pass
sys.exit('GOOGLE_PLACES_API_KEY not found in env or secrets registry')
KEY = load_key()
ENDPOINT = 'https://maps.googleapis.com/maps/api/place/findplacefromtext/json'
USER_AGENT = 'lacountyeats/1.0'
def find_place(name, city, state):
q = f'{name} {city} {state}'
params = {
'input': q,
'inputtype': 'textquery',
'fields': 'place_id,name,website,formatted_address,rating,user_ratings_total',
'key': KEY,
}
url = ENDPOINT + '?' + urllib.parse.urlencode(params)
req = urllib.request.Request(url, headers={'User-Agent': USER_AGENT})
try:
with urllib.request.urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
if data.get('status') == 'OK' and data.get('candidates'):
return data['candidates'][0]
if data.get('status') in ('OVER_QUERY_LIMIT', 'REQUEST_DENIED'):
print(f' ! Google Places error: {data.get("status")} {data.get("error_message", "")}', file=sys.stderr)
sys.exit(2)
return None
except urllib.error.HTTPError as e:
if e.code == 429:
time.sleep(5)
return None
except Exception:
return None
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--limit', type=int, default=500)
ap.add_argument('--dry-run', action='store_true')
args = ap.parse_args()
with psycopg.connect(DB_URL) as conn:
with conn.cursor() as cur:
cur.execute("""
SELECT f.facility_id, f.name, COALESCE(f.city,''), COALESCE(f.state,'CA')
FROM facility f
LEFT JOIN facility_enrichment fe ON fe.facility_id = f.facility_id
WHERE (fe.website IS NULL OR fe.website = '')
AND f.name IS NOT NULL
AND f.city IS NOT NULL
ORDER BY f.facility_id
LIMIT %s
""", (args.limit,))
facilities = cur.fetchall()
print(f'[gplaces] {len(facilities)} facilities to enrich')
hits = misses = no_website = 0
for i, (fid, name, city, state) in enumerate(facilities, 1):
cand = find_place(name, city, state)
if not cand:
misses += 1
if i % 50 == 0:
print(f' [{i}/{len(facilities)}] hits={hits} no_website={no_website} no_match={misses}')
time.sleep(0.05)
continue
website = cand.get('website', '')
place_id = cand.get('place_id', '')
addr = cand.get('formatted_address', '')
rating = cand.get('rating')
n_ratings = cand.get('user_ratings_total', 0)
if not website:
no_website += 1
print(f' [{i}/{len(facilities)}] · {name[:35]} → matched but no website ({addr[:40]})')
else:
hits += 1
if not args.dry_run:
with conn.cursor() as cur:
cur.execute("""
INSERT INTO facility_enrichment (facility_id, website, google_place_id, rating_google, review_count_total)
VALUES (%s, %s, %s, %s, %s)
ON CONFLICT (facility_id) DO UPDATE SET
website = EXCLUDED.website,
google_place_id = COALESCE(facility_enrichment.google_place_id, EXCLUDED.google_place_id),
rating_google = COALESCE(EXCLUDED.rating_google, facility_enrichment.rating_google),
review_count_total = COALESCE(EXCLUDED.review_count_total, facility_enrichment.review_count_total)
""", (fid, website, place_id, rating, n_ratings))
conn.commit()
print(f' [{i}/{len(facilities)}] ✓ {name[:35]} → {website[:50]}')
sys.stdout.flush()
time.sleep(0.05)
print(f'[gplaces] done · hits={hits} · matched-no-website={no_website} · no-match={misses} · hit-rate={100*hits/max(len(facilities),1):.1f}%')
approx_cost = len(facilities) * 0.017
print(f'[gplaces] approx cost: ${approx_cost:.2f} ({len(facilities)} Find Place calls @ $0.017)')
if __name__ == '__main__':
main()