Patterns for location-based queries: "near me", "within X km",
bounding boxes, and proximity calculations across Montréal datasets.
All coordinates are WGS84 unless noted.
/ Stratégies pour requêtes spatiales : « près de moi », « dans un rayon
de X km », boîtes englobantes et calculs de proximité.
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
Patterns for location-based queries: "near me", "within X km",
bounding boxes, and proximity calculations across Montréal datasets.
All coordinates are WGS84 unless noted.
/ Stratégies pour requêtes spatiales : « près de moi », « dans un rayon
de X km », boîtes englobantes et calculs de proximité.
triggers
["near, nearby, within, radius, distance, closest, km, meters, around","près, proche, dans un rayon, distance, plus proche, km, mètres, autour"]
Spatial Queries / Requêtes spatiales
The SQL Proximity Pattern / Patron de proximité SQL
CKAN DataStore doesn't have PostGIS, so you can't use ST_DWithin. Instead, use a bounding box filter in SQL followed by Haversine client-side.
Step 1: Bounding Box (server-side, fast)
Convert "within X km" to a lat/lon bounding box:
defbbox(lat, lon, radius_km):
"""Approximate bounding box for a radius around a point."""# 1 degree latitude ≈ 111.32 km# 1 degree longitude ≈ 111.32 * cos(lat) km at Montreal's latitudeimport math
dlat = radius_km / 111.32
dlon = radius_km / (111.32 * math.cos(math.radians(lat)))
return {
'lat_min': lat - dlat,
'lat_max': lat + dlat,
'lon_min': lon - dlon,
: lon + dlon,
}
b = bbox(, -, )
'lon_max'
# Example: 2km around Mile End (45.524, -73.596)
45.524
73.596
2.0
# → lat: 45.506 to 45.542, lon: -73.622 to -73.570
import math
defhaversine_m(lat1, lon1, lat2, lon2):
"""Distance in meters between two WGS84 points."""
R = 6371000
p1, p2 = math.radians(lat1), math.radians(lat2)
dp = math.radians(lat2 - lat1)
dl = math.radians(lon2 - lon1)
a = math.sin(dp/2)**2 + math.cos(p1)*math.cos(p2)*math.sin(dl/2)**2return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
# Filter bbox results to exact radius
center_lat, center_lon, radius_m = 45.524, -73.596, 2000
results = [
r for r in bbox_results
if haversine_m(center_lat, center_lon,
float(r['Latitude']), float(r['Longitude'])) <= radius_m
]
Common Spatial Queries / Requêtes spatiales courantes
"Trees near me" / "Arbres près de moi"
b = bbox(user_lat, user_lon, 0.5) # 500m radius
trees = ckan_sql(f'''
SELECT "Essence_fr", "Essence_ang", "DHP",
"Latitude", "Longitude", "ARROND_NOM"
FROM "64e28fe6-ef37-437a-972d-d1d3f1f7d891"
WHERE CAST("Latitude" AS FLOAT) BETWEEN {b['lat_min']} AND {b['lat_max']}
AND CAST("Longitude" AS FLOAT) BETWEEN {b['lon_min']} AND {b['lon_max']}
LIMIT 5000
''')
"Crime near this address" / "Criminalité près de cette adresse"
# First geocode the address (see address-geocoding skill)
addr_lat, addr_lon = geocode_nominatim("3575 Parc Avenue")
b = bbox(addr_lat, addr_lon, 1.0) # 1km radius
crimes = ckan_sql(f'''
SELECT "CATEGORIE", "DATE", "QUART", "LATITUDE", "LONGITUDE"
FROM "c6f482bf-bf0f-4960-8b2f-9ca22b2d4e88"
WHERE CAST("LATITUDE" AS FLOAT) BETWEEN {b['lat_min']} AND {b['lat_max']}
AND CAST("LONGITUDE" AS FLOAT) BETWEEN {b['lon_min']} AND {b['lon_max']}
AND "DATE" >= '2025-01-01'
LIMIT 5000
''')
"Nearest BIXI station" / "Station BIXI la plus proche"
import requests
stations = requests.get('https://gbfs.velobixi.com/gbfs/en/station_information.json').json()
status = requests.get('https://gbfs.velobixi.com/gbfs/en/station_status.json').json()
# Build lookup for bike availability
avail = {s['station_id']: s['num_bikes_available']
for s in status['data']['stations']}
# Find nearest with bikes
nearest = sorted(
stations['data']['stations'],
key=lambda s: haversine_m(user_lat, user_lon, s['lat'], s['lon'])
)
for s in nearest[:5]:
dist = haversine_m(user_lat, user_lon, s['lat'], s['lon'])
bikes = avail.get(s['station_id'], 0)
print(f" {s['name']}: {dist:.0f}m away, {bikes} bikes")
"Fire stations within 3km" / "Casernes dans un rayon de 3 km"
b = bbox(user_lat, user_lon, 3.0)
casernes = ckan_sql(f'''
SELECT * FROM "RESOURCE_UUID_FOR_CASERNES"
WHERE CAST("LATITUDE" AS FLOAT) BETWEEN {b['lat_min']} AND {b['lat_max']}
AND CAST("LONGITUDE" AS FLOAT) BETWEEN {b['lon_min']} AND {b['lon_max']}
''')