"""Google Places sync for Export Intel (optional — requires GOOGLE_MAPS_API_KEY).""" from __future__ import annotations import json import os import time from typing import Any, Optional import httpx from app.db import fetch_one from app.export_intel_sync import _get_territory, _upsert_contact PLACES_URL = "https://maps.googleapis.com/maps/api/place/textsearch/json" DETAILS_URL = "https://maps.googleapis.com/maps/api/place/details/json" QUERIES = [ "meat wholesaler", "halal meat distributor", "poultry distributor", "frozen food supplier", "contract catering", "food importer", "halal butcher", ] LARGE_COUNTRIES = frozenset({"US", "CA", "BR", "MX", "AR", "AU", "RU", "CN", "IN", "CD", "DZ", "EG", "IR", "SA"}) def _api_key() -> Optional[str]: return os.getenv("GOOGLE_MAPS_API_KEY") or os.getenv("GOOGLE_PLACES_API_KEY") def _places_search(query: str, lat: float, lon: float, radius_m: int = 80000) -> list[dict[str, Any]]: key = _api_key() if not key: return [] params = { "query": query, "location": f"{lat},{lon}", "radius": radius_m, "key": key, } with httpx.Client(timeout=30.0) as client: resp = client.get(PLACES_URL, params=params) if resp.status_code >= 400: return [] data = resp.json() if data.get("status") not in ("OK", "ZERO_RESULTS"): return [] return data.get("results", [])[:12] def _place_details(place_id: str) -> dict[str, Any]: key = _api_key() if not key: return {} with httpx.Client(timeout=20.0) as client: resp = client.get( DETAILS_URL, params={ "place_id": place_id, "fields": "name,formatted_phone_number,website,formatted_address,geometry,international_phone_number", "key": key, }, ) if resp.status_code >= 400: return {} return resp.json().get("result", {}) def _bbox_points(bbox: Any, grid: int = 3) -> list[tuple[float, float]]: if isinstance(bbox, str): bbox = json.loads(bbox) if not bbox or len(bbox) != 4: return [] south, west, north, east = bbox points: list[tuple[float, float]] = [] for i in range(grid): for j in range(grid): lat = south + (north - south) * (i + 0.5) / grid lon = west + (east - west) * (j + 0.5) / grid points.append((lat, lon)) return points def _upsert_place(place: dict[str, Any], details: dict[str, Any], country_iso2: str, territory_code: str, query: str) -> tuple[str, int]: from app.db import execute, execute_returning pid = place.get("place_id") name = details.get("name") or place.get("name") or "Unknown" geo = details.get("geometry", {}).get("location", place.get("geometry", {}).get("location", {})) plat = geo.get("lat") plon = geo.get("lng") phone = details.get("formatted_phone_number") or details.get("international_phone_number") website = details.get("website") entity_type = "contract_caterer" if "catering" in query else "distributor" existing = fetch_one( "SELECT id FROM export_market_entities WHERE metadata->>'google_place_id' = %s", (pid,), ) if existing: execute( """ UPDATE export_market_entities SET phone = COALESCE(%s, phone), website = COALESCE(%s, website), lat = COALESCE(%s, lat), lon = COALESCE(%s, lon), confidence = GREATEST(confidence, 65), updated_at = NOW() WHERE id = %s """, (phone, website, plat, plon, existing["id"]), ) entity_id = existing["id"] action = "updated" else: row = execute_returning( """ INSERT INTO export_market_entities ( name, entity_type, country_iso2, territory_code, lat, lon, phone, website, google_place_id, source, sources, confidence, metadata ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,'google_places','["google_places"]'::jsonb,65,%s) RETURNING id """, ( name[:255], entity_type, country_iso2.upper(), territory_code, plat, plon, phone, website, pid, json.dumps({"google_place_id": pid}), ), ) entity_id = row["id"] action = "inserted" if phone: _upsert_contact(entity_id, None, phone, "google_places", 70, role="sales", name="Google Places") return action, entity_id def sync_places_country(country_iso2: str) -> dict[str, Any]: territory = _get_territory(country_iso2) if not territory or not territory.get("lat"): return {"skipped": True, "reason": "no territory"} if not _api_key(): return {"skipped": True, "reason": "GOOGLE_MAPS_API_KEY not set"} territory_code = territory["code"] inserted = updated = 0 seen: set[str] = set() iso = country_iso2.upper() if iso in LARGE_COUNTRIES and territory.get("bbox"): search_points = _bbox_points(territory["bbox"], grid=3) else: search_points = [(territory["lat"], territory["lon"])] radius = 60000 if iso in LARGE_COUNTRIES else 80000 for lat, lon in search_points: for q in QUERIES: for place in _places_search(q, lat, lon, radius_m=radius): pid = place.get("place_id") if not pid or pid in seen: continue seen.add(pid) details = _place_details(pid) action, _ = _upsert_place(place, details, iso, territory_code, q) if action == "inserted": inserted += 1 else: updated += 1 time.sleep(0.15) time.sleep(0.5) return {"inserted": inserted, "updated": updated, "queries": len(QUERIES), "search_points": len(search_points)} def sync_places_countries(countries: list[str]) -> dict[str, Any]: if not _api_key(): return {"skipped": True, "reason": "GOOGLE_MAPS_API_KEY not set"} out: dict[str, Any] = {} for c in countries: out[c] = sync_places_country(c) time.sleep(1) return out