Sync all skills and memories 2026-04-14 07:27
This commit is contained in:
184
skills/leisure/find-nearby/scripts/find_nearby.py
Normal file
184
skills/leisure/find-nearby/scripts/find_nearby.py
Normal file
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Find nearby places using OpenStreetMap (Overpass + Nominatim). No API keys needed.
|
||||
|
||||
Usage:
|
||||
# By coordinates
|
||||
python find_nearby.py --lat 36.17 --lon -115.14 --type restaurant --radius 1500
|
||||
|
||||
# By address/city/zip (auto-geocoded)
|
||||
python find_nearby.py --near "Times Square, New York" --type cafe --radius 1000
|
||||
python find_nearby.py --near "90210" --type pharmacy
|
||||
|
||||
# Multiple types
|
||||
python find_nearby.py --lat 36.17 --lon -115.14 --type restaurant --type bar
|
||||
|
||||
# JSON output for programmatic use
|
||||
python find_nearby.py --near "downtown las vegas" --type restaurant --json
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
OVERPASS_URLS = [
|
||||
"https://overpass-api.de/api/interpreter",
|
||||
"https://overpass.kumi.systems/api/interpreter",
|
||||
]
|
||||
NOMINATIM_URL = "https://nominatim.openstreetmap.org/search"
|
||||
USER_AGENT = "HermesAgent/1.0 (find-nearby skill)"
|
||||
TIMEOUT = 15
|
||||
|
||||
|
||||
def _http_get(url: str) -> Any:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
||||
with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
def _http_post(url: str, data: str) -> Any:
|
||||
req = urllib.request.Request(
|
||||
url, data=data.encode(), headers={"User-Agent": USER_AGENT}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
def haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
||||
"""Distance in meters between two coordinates."""
|
||||
R = 6_371_000
|
||||
rlat1, rlat2 = math.radians(lat1), math.radians(lat2)
|
||||
dlat = math.radians(lat2 - lat1)
|
||||
dlon = math.radians(lon2 - lon1)
|
||||
a = math.sin(dlat / 2) ** 2 + math.cos(rlat1) * math.cos(rlat2) * math.sin(dlon / 2) ** 2
|
||||
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
||||
|
||||
|
||||
def geocode(query: str) -> tuple[float, float]:
|
||||
"""Convert address/city/zip to coordinates via Nominatim."""
|
||||
params = urllib.parse.urlencode({"q": query, "format": "json", "limit": 1})
|
||||
results = _http_get(f"{NOMINATIM_URL}?{params}")
|
||||
if not results:
|
||||
print(f"Error: Could not geocode '{query}'. Try a more specific address.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return float(results[0]["lat"]), float(results[0]["lon"])
|
||||
|
||||
|
||||
def find_nearby(lat: float, lon: float, types: list[str], radius: int = 1500, limit: int = 15) -> list[dict]:
|
||||
"""Query Overpass for nearby amenities."""
|
||||
# Build Overpass QL query
|
||||
type_filters = "".join(
|
||||
f'nwr["amenity"="{t}"](around:{radius},{lat},{lon});' for t in types
|
||||
)
|
||||
query = f"[out:json][timeout:{TIMEOUT}];({type_filters});out center tags;"
|
||||
|
||||
# Try each Overpass server
|
||||
data = None
|
||||
for url in OVERPASS_URLS:
|
||||
try:
|
||||
data = _http_post(url, f"data={urllib.parse.quote(query)}")
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not data:
|
||||
return []
|
||||
|
||||
# Parse results
|
||||
places = []
|
||||
for el in data.get("elements", []):
|
||||
tags = el.get("tags", {})
|
||||
name = tags.get("name")
|
||||
if not name:
|
||||
continue
|
||||
|
||||
# Get coordinates (nodes have lat/lon directly, ways/relations use center)
|
||||
plat = el.get("lat") or (el.get("center", {}) or {}).get("lat")
|
||||
plon = el.get("lon") or (el.get("center", {}) or {}).get("lon")
|
||||
if not plat or not plon:
|
||||
continue
|
||||
|
||||
dist = haversine(lat, lon, plat, plon)
|
||||
|
||||
place = {
|
||||
"name": name,
|
||||
"type": tags.get("amenity", ""),
|
||||
"distance_m": round(dist),
|
||||
"lat": plat,
|
||||
"lon": plon,
|
||||
"maps_url": f"https://www.google.com/maps/search/?api=1&query={plat},{plon}",
|
||||
"directions_url": f"https://www.google.com/maps/dir/?api=1&origin={lat},{lon}&destination={plat},{plon}",
|
||||
}
|
||||
|
||||
# Add useful optional fields
|
||||
if tags.get("cuisine"):
|
||||
place["cuisine"] = tags["cuisine"]
|
||||
if tags.get("opening_hours"):
|
||||
place["hours"] = tags["opening_hours"]
|
||||
if tags.get("phone"):
|
||||
place["phone"] = tags["phone"]
|
||||
if tags.get("website"):
|
||||
place["website"] = tags["website"]
|
||||
if tags.get("addr:street"):
|
||||
addr_parts = [tags.get("addr:housenumber", ""), tags.get("addr:street", "")]
|
||||
if tags.get("addr:city"):
|
||||
addr_parts.append(tags["addr:city"])
|
||||
place["address"] = " ".join(p for p in addr_parts if p)
|
||||
|
||||
places.append(place)
|
||||
|
||||
# Sort by distance, limit results
|
||||
places.sort(key=lambda p: p["distance_m"])
|
||||
return places[:limit]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Find nearby places via OpenStreetMap")
|
||||
parser.add_argument("--lat", type=float, help="Latitude")
|
||||
parser.add_argument("--lon", type=float, help="Longitude")
|
||||
parser.add_argument("--near", type=str, help="Address, city, or zip code (geocoded automatically)")
|
||||
parser.add_argument("--type", action="append", dest="types", default=[], help="Place type (restaurant, cafe, bar, pharmacy, etc.)")
|
||||
parser.add_argument("--radius", type=int, default=1500, help="Search radius in meters (default: 1500)")
|
||||
parser.add_argument("--limit", type=int, default=15, help="Max results (default: 15)")
|
||||
parser.add_argument("--json", action="store_true", dest="json_output", help="Output as JSON")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Resolve coordinates
|
||||
if args.near:
|
||||
lat, lon = geocode(args.near)
|
||||
elif args.lat is not None and args.lon is not None:
|
||||
lat, lon = args.lat, args.lon
|
||||
else:
|
||||
print("Error: Provide --lat/--lon or --near", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not args.types:
|
||||
args.types = ["restaurant"]
|
||||
|
||||
places = find_nearby(lat, lon, args.types, args.radius, args.limit)
|
||||
|
||||
if args.json_output:
|
||||
print(json.dumps({"origin": {"lat": lat, "lon": lon}, "results": places, "count": len(places)}, indent=2))
|
||||
else:
|
||||
if not places:
|
||||
print(f"No {'/'.join(args.types)} found within {args.radius}m")
|
||||
return
|
||||
print(f"Found {len(places)} places within {args.radius}m:\n")
|
||||
for i, p in enumerate(places, 1):
|
||||
dist_str = f"{p['distance_m']}m" if p["distance_m"] < 1000 else f"{p['distance_m']/1000:.1f}km"
|
||||
print(f" {i}. {p['name']} ({p['type']}) — {dist_str}")
|
||||
if p.get("cuisine"):
|
||||
print(f" Cuisine: {p['cuisine']}")
|
||||
if p.get("hours"):
|
||||
print(f" Hours: {p['hours']}")
|
||||
if p.get("address"):
|
||||
print(f" Address: {p['address']}")
|
||||
print(f" Map: {p['maps_url']}")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user