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:
Teknium 2026-04-21 00:52:25 -07:00 committed by GitHub
parent c312e8ecf5
commit c5a814b233
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 71 additions and 26 deletions

View file

@ -2,7 +2,7 @@
name: maps
description: >
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
box + area for a named place, and POI search within a rectangle.
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
```
44 categories: restaurant, cafe, bar, hospital, pharmacy, hotel, supermarket,
atm, gas_station, parking, museum, park, school, university, bank, police,
fire_station, library, airport, train_station, bus_stop, church, mosque,
synagogue, dentist, doctor, cinema, theatre, gym, swimming_pool, post_office,
convenience_store, bakery, bookshop, laundry, car_wash, car_rental,
bicycle_rental, taxi, veterinary, zoo, playground, stadium, nightclub.
46 categories: restaurant, cafe, bar, hospital, pharmacy, hotel, guest_house,
camp_site, supermarket, atm, gas_station, parking, museum, park, school,
university, bank, police, fire_station, library, airport, train_station,
bus_stop, church, mosque, synagogue, dentist, doctor, cinema, theatre, gym,
swimming_pool, post_office, convenience_store, bakery, bookshop, laundry,
car_wash, car_rental, bicycle_rental, taxi, veterinary, zoo, playground,
stadium, nightclub.
Each result includes: `name`, `address`, `lat`/`lon`, `distance_m`,
`maps_url` (clickable Google Maps link), `directions_url` (Google Maps

View file

@ -58,7 +58,9 @@ CATEGORY_TAGS = {
"restaurant": ("amenity", "restaurant"),
"cafe": ("amenity", "cafe"),
"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"),
# Health
"hospital": ("amenity", "hospital"),
@ -68,6 +70,8 @@ CATEGORY_TAGS = {
"veterinary": ("amenity", "veterinary"),
# Accommodation
"hotel": ("tourism", "hotel"),
"guest_house": ("tourism", "guest_house"),
"camp_site": ("tourism", "camp_site"),
# Shopping & Services
"supermarket": ("shop", "supermarket"),
"bookshop": ("shop", "books"),
@ -120,6 +124,19 @@ RELIGION_FILTER = {
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 = {
"driving": "driving",
"walking": "foot",
@ -338,36 +355,63 @@ def geocode_single(query):
# ---------------------------------------------------------------------------
def build_overpass_nearby(tag_key, tag_val, lat, lon, radius, limit,
religion=None):
"""Build an Overpass QL query for nearby POIs around a point."""
religion=None, tag_pairs=None):
"""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 = ""
if 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 (
f'[out:json][timeout:25];\n'
f'(\n'
f' node["{tag_key}"="{tag_val}"]{religion_filter}'
f'(around:{radius},{lat},{lon});\n'
f' way["{tag_key}"="{tag_val}"]{religion_filter}'
f'(around:{radius},{lat},{lon});\n'
f'{body}\n'
f');\n'
f'out center {limit};\n'
)
def build_overpass_bbox(tag_key, tag_val, south, west, north, east, limit,
religion=None):
"""Build an Overpass QL query for POIs within a bounding box."""
religion=None, tag_pairs=None):
"""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 = ""
if 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 (
f'[out:json][timeout:25];\n'
f'(\n'
f' node["{tag_key}"="{tag_val}"]{religion_filter}'
f'({south},{west},{north},{east});\n'
f' way["{tag_key}"="{tag_val}"]{religion_filter}'
f'({south},{west},{north},{east});\n'
f'{body}\n'
f');\n'
f'out center {limit};\n'
)
@ -605,10 +649,10 @@ def cmd_nearby(args):
# appear twice.
merged = {}
for category in categories:
tag_key, tag_val = CATEGORY_TAGS[category]
tag_pairs = _tags_for(category)
religion = RELIGION_FILTER.get(category)
query = build_overpass_nearby(tag_key, tag_val, lat, lon, radius, limit,
religion=religion)
query = build_overpass_nearby(None, None, lat, lon, radius, limit,
religion=religion, tag_pairs=tag_pairs)
raw = overpass_query(query)
elements = raw.get("elements", [])
for place in parse_overpass_elements(elements, ref_lat=lat, ref_lon=lon):
@ -945,10 +989,10 @@ def cmd_bbox(args):
if limit <= 0:
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)
query = build_overpass_bbox(tag_key, tag_val, south, west, north, east,
limit, religion=religion)
query = build_overpass_bbox(None, None, south, west, north, east,
limit, religion=religion, tag_pairs=tag_pairs)
raw = overpass_query(query)