mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
feat(maps): add guest_house, camp_site, and dual-key bakery lookup (#13398)
Small follow-up inspired by stale PR #2421 (@poojandpatel). - bakery now searches both shop=bakery AND amenity=bakery in one Overpass query so indie bakeries tagged either way are returned. Reproduces #2421's Lawrenceville, NJ test case (The Gingered Peach, WildFlour Bakery). - Adds tourism=guest_house and tourism=camp_site as first-class categories. - CATEGORY_TAGS entries can now be a list of (key, value) tuples; new _tags_for() normaliser + tag_pairs= kwarg on build_overpass_nearby/bbox union the results in one query. Old single-tuple call sites unchanged (back-compat preserved). - SKILL.md: 44 → 46 categories, list updated.
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
name: maps
|
name: maps
|
||||||
description: >
|
description: >
|
||||||
Location intelligence — geocode a place, reverse-geocode coordinates,
|
Location intelligence — geocode a place, reverse-geocode coordinates,
|
||||||
find nearby places (44 POI categories), driving/walking/cycling
|
find nearby places (46 POI categories), driving/walking/cycling
|
||||||
distance + time, turn-by-turn directions, timezone lookup, bounding
|
distance + time, turn-by-turn directions, timezone lookup, bounding
|
||||||
box + area for a named place, and POI search within a rectangle.
|
box + area for a named place, and POI search within a rectangle.
|
||||||
Uses OpenStreetMap + Overpass + OSRM. Free, no API key.
|
Uses OpenStreetMap + Overpass + OSRM. Free, no API key.
|
||||||
@@ -83,12 +83,13 @@ python3 $MAPS nearby --near "90210" --category pharmacy
|
|||||||
python3 $MAPS nearby --near "downtown austin" --category restaurant --category bar --limit 10
|
python3 $MAPS nearby --near "downtown austin" --category restaurant --category bar --limit 10
|
||||||
```
|
```
|
||||||
|
|
||||||
44 categories: restaurant, cafe, bar, hospital, pharmacy, hotel, supermarket,
|
46 categories: restaurant, cafe, bar, hospital, pharmacy, hotel, guest_house,
|
||||||
atm, gas_station, parking, museum, park, school, university, bank, police,
|
camp_site, supermarket, atm, gas_station, parking, museum, park, school,
|
||||||
fire_station, library, airport, train_station, bus_stop, church, mosque,
|
university, bank, police, fire_station, library, airport, train_station,
|
||||||
synagogue, dentist, doctor, cinema, theatre, gym, swimming_pool, post_office,
|
bus_stop, church, mosque, synagogue, dentist, doctor, cinema, theatre, gym,
|
||||||
convenience_store, bakery, bookshop, laundry, car_wash, car_rental,
|
swimming_pool, post_office, convenience_store, bakery, bookshop, laundry,
|
||||||
bicycle_rental, taxi, veterinary, zoo, playground, stadium, nightclub.
|
car_wash, car_rental, bicycle_rental, taxi, veterinary, zoo, playground,
|
||||||
|
stadium, nightclub.
|
||||||
|
|
||||||
Each result includes: `name`, `address`, `lat`/`lon`, `distance_m`,
|
Each result includes: `name`, `address`, `lat`/`lon`, `distance_m`,
|
||||||
`maps_url` (clickable Google Maps link), `directions_url` (Google Maps
|
`maps_url` (clickable Google Maps link), `directions_url` (Google Maps
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ CATEGORY_TAGS = {
|
|||||||
"restaurant": ("amenity", "restaurant"),
|
"restaurant": ("amenity", "restaurant"),
|
||||||
"cafe": ("amenity", "cafe"),
|
"cafe": ("amenity", "cafe"),
|
||||||
"bar": ("amenity", "bar"),
|
"bar": ("amenity", "bar"),
|
||||||
"bakery": ("shop", "bakery"),
|
# bakery is tagged as shop=bakery in the OSM wiki, but some mappers use
|
||||||
|
# amenity=bakery. Search both so small indie bakeries aren't missed.
|
||||||
|
"bakery": [("shop", "bakery"), ("amenity", "bakery")],
|
||||||
"convenience_store": ("shop", "convenience"),
|
"convenience_store": ("shop", "convenience"),
|
||||||
# Health
|
# Health
|
||||||
"hospital": ("amenity", "hospital"),
|
"hospital": ("amenity", "hospital"),
|
||||||
@@ -68,6 +70,8 @@ CATEGORY_TAGS = {
|
|||||||
"veterinary": ("amenity", "veterinary"),
|
"veterinary": ("amenity", "veterinary"),
|
||||||
# Accommodation
|
# Accommodation
|
||||||
"hotel": ("tourism", "hotel"),
|
"hotel": ("tourism", "hotel"),
|
||||||
|
"guest_house": ("tourism", "guest_house"),
|
||||||
|
"camp_site": ("tourism", "camp_site"),
|
||||||
# Shopping & Services
|
# Shopping & Services
|
||||||
"supermarket": ("shop", "supermarket"),
|
"supermarket": ("shop", "supermarket"),
|
||||||
"bookshop": ("shop", "books"),
|
"bookshop": ("shop", "books"),
|
||||||
@@ -120,6 +124,19 @@ RELIGION_FILTER = {
|
|||||||
|
|
||||||
VALID_CATEGORIES = sorted(CATEGORY_TAGS.keys())
|
VALID_CATEGORIES = sorted(CATEGORY_TAGS.keys())
|
||||||
|
|
||||||
|
|
||||||
|
def _tags_for(category):
|
||||||
|
"""Return the CATEGORY_TAGS entry as a list of (key, value) pairs.
|
||||||
|
|
||||||
|
Most categories map to a single (tag_key, tag_val) tuple, but some
|
||||||
|
(e.g. ``bakery``) are tagged under more than one OSM key and are
|
||||||
|
represented as a list of tuples. Normalise both forms to a list.
|
||||||
|
"""
|
||||||
|
entry = CATEGORY_TAGS[category]
|
||||||
|
if isinstance(entry, list):
|
||||||
|
return list(entry)
|
||||||
|
return [entry]
|
||||||
|
|
||||||
OSRM_PROFILES = {
|
OSRM_PROFILES = {
|
||||||
"driving": "driving",
|
"driving": "driving",
|
||||||
"walking": "foot",
|
"walking": "foot",
|
||||||
@@ -338,36 +355,63 @@ def geocode_single(query):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def build_overpass_nearby(tag_key, tag_val, lat, lon, radius, limit,
|
def build_overpass_nearby(tag_key, tag_val, lat, lon, radius, limit,
|
||||||
religion=None):
|
religion=None, tag_pairs=None):
|
||||||
"""Build an Overpass QL query for nearby POIs around a point."""
|
"""Build an Overpass QL query for nearby POIs around a point.
|
||||||
|
|
||||||
|
If ``tag_pairs`` is provided, the query unions across every
|
||||||
|
``(key, value)`` pair (used for categories like ``bakery`` that are
|
||||||
|
tagged under more than one OSM key). Otherwise falls back to the
|
||||||
|
single ``tag_key``/``tag_val`` pair for back-compat.
|
||||||
|
"""
|
||||||
|
pairs = tag_pairs if tag_pairs else [(tag_key, tag_val)]
|
||||||
religion_filter = ""
|
religion_filter = ""
|
||||||
if religion:
|
if religion:
|
||||||
religion_filter = f'["religion"="{religion}"]'
|
religion_filter = f'["religion"="{religion}"]'
|
||||||
|
body_lines = []
|
||||||
|
for k, v in pairs:
|
||||||
|
body_lines.append(
|
||||||
|
f' node["{k}"="{v}"]{religion_filter}'
|
||||||
|
f'(around:{radius},{lat},{lon});'
|
||||||
|
)
|
||||||
|
body_lines.append(
|
||||||
|
f' way["{k}"="{v}"]{religion_filter}'
|
||||||
|
f'(around:{radius},{lat},{lon});'
|
||||||
|
)
|
||||||
|
body = "\n".join(body_lines)
|
||||||
return (
|
return (
|
||||||
f'[out:json][timeout:25];\n'
|
f'[out:json][timeout:25];\n'
|
||||||
f'(\n'
|
f'(\n'
|
||||||
f' node["{tag_key}"="{tag_val}"]{religion_filter}'
|
f'{body}\n'
|
||||||
f'(around:{radius},{lat},{lon});\n'
|
|
||||||
f' way["{tag_key}"="{tag_val}"]{religion_filter}'
|
|
||||||
f'(around:{radius},{lat},{lon});\n'
|
|
||||||
f');\n'
|
f');\n'
|
||||||
f'out center {limit};\n'
|
f'out center {limit};\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_overpass_bbox(tag_key, tag_val, south, west, north, east, limit,
|
def build_overpass_bbox(tag_key, tag_val, south, west, north, east, limit,
|
||||||
religion=None):
|
religion=None, tag_pairs=None):
|
||||||
"""Build an Overpass QL query for POIs within a bounding box."""
|
"""Build an Overpass QL query for POIs within a bounding box.
|
||||||
|
|
||||||
|
See ``build_overpass_nearby`` for ``tag_pairs`` semantics.
|
||||||
|
"""
|
||||||
|
pairs = tag_pairs if tag_pairs else [(tag_key, tag_val)]
|
||||||
religion_filter = ""
|
religion_filter = ""
|
||||||
if religion:
|
if religion:
|
||||||
religion_filter = f'["religion"="{religion}"]'
|
religion_filter = f'["religion"="{religion}"]'
|
||||||
|
body_lines = []
|
||||||
|
for k, v in pairs:
|
||||||
|
body_lines.append(
|
||||||
|
f' node["{k}"="{v}"]{religion_filter}'
|
||||||
|
f'({south},{west},{north},{east});'
|
||||||
|
)
|
||||||
|
body_lines.append(
|
||||||
|
f' way["{k}"="{v}"]{religion_filter}'
|
||||||
|
f'({south},{west},{north},{east});'
|
||||||
|
)
|
||||||
|
body = "\n".join(body_lines)
|
||||||
return (
|
return (
|
||||||
f'[out:json][timeout:25];\n'
|
f'[out:json][timeout:25];\n'
|
||||||
f'(\n'
|
f'(\n'
|
||||||
f' node["{tag_key}"="{tag_val}"]{religion_filter}'
|
f'{body}\n'
|
||||||
f'({south},{west},{north},{east});\n'
|
|
||||||
f' way["{tag_key}"="{tag_val}"]{religion_filter}'
|
|
||||||
f'({south},{west},{north},{east});\n'
|
|
||||||
f');\n'
|
f');\n'
|
||||||
f'out center {limit};\n'
|
f'out center {limit};\n'
|
||||||
)
|
)
|
||||||
@@ -605,10 +649,10 @@ def cmd_nearby(args):
|
|||||||
# appear twice.
|
# appear twice.
|
||||||
merged = {}
|
merged = {}
|
||||||
for category in categories:
|
for category in categories:
|
||||||
tag_key, tag_val = CATEGORY_TAGS[category]
|
tag_pairs = _tags_for(category)
|
||||||
religion = RELIGION_FILTER.get(category)
|
religion = RELIGION_FILTER.get(category)
|
||||||
query = build_overpass_nearby(tag_key, tag_val, lat, lon, radius, limit,
|
query = build_overpass_nearby(None, None, lat, lon, radius, limit,
|
||||||
religion=religion)
|
religion=religion, tag_pairs=tag_pairs)
|
||||||
raw = overpass_query(query)
|
raw = overpass_query(query)
|
||||||
elements = raw.get("elements", [])
|
elements = raw.get("elements", [])
|
||||||
for place in parse_overpass_elements(elements, ref_lat=lat, ref_lon=lon):
|
for place in parse_overpass_elements(elements, ref_lat=lat, ref_lon=lon):
|
||||||
@@ -945,10 +989,10 @@ def cmd_bbox(args):
|
|||||||
if limit <= 0:
|
if limit <= 0:
|
||||||
error_exit("Limit must be a positive integer.")
|
error_exit("Limit must be a positive integer.")
|
||||||
|
|
||||||
tag_key, tag_val = CATEGORY_TAGS[category]
|
tag_pairs = _tags_for(category)
|
||||||
religion = RELIGION_FILTER.get(category)
|
religion = RELIGION_FILTER.get(category)
|
||||||
query = build_overpass_bbox(tag_key, tag_val, south, west, north, east,
|
query = build_overpass_bbox(None, None, south, west, north, east,
|
||||||
limit, religion=religion)
|
limit, religion=religion, tag_pairs=tag_pairs)
|
||||||
|
|
||||||
raw = overpass_query(query)
|
raw = overpass_query(query)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user