Skip to content
Back to skills

Geo Validation

ASecurity

Validate directional claims, walk times, and pedestrian routes using OSM data (Nominatim geocoding + Overpass queries). Fixes hallucinations about east/west/north/south, crossing barriers, and travel times by grounding everything in real coordinates.

  • 8 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 20, 2026
developmentpythonbashnodegitapi

Works with

  • cli
  • api

Security analysis

A92/100
  • mediumUses curl or wget to download content
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 20, 2026

npx -y skills add tstapler/dotfiles --skill geo-validation --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Geo Validation?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Geo Validation
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tstapler-geo-validation/badge)](https://www.skillsdirectory.com/skills/tstapler-geo-validation)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: geo-validation
description: Validate directional claims, walk times, and pedestrian routes using OSM data (Nominatim geocoding + Overpass queries). Fixes hallucinations about east/west/north/south, crossing barriers, and travel times by grounding everything in real coordinates.
---

# Geospatial Validation Skill

Use this skill whenever writing or reviewing content that makes claims about:
- Cardinal directions between real places ("X is north of Y", "walk east to Z")
- Walk, bike, or transit times between locations
- Pedestrian route feasibility (especially near major roads, highways, water)
- Which side of a road, barrier, or landmark a place is on

## When to Trigger

- Any document contains directional language about real-world places
- Walk times are stated or implied
- A route is described that might cross a major road or natural barrier
- Content was written without first looking up coordinates
- A previous version of the content contained a directional error

**Do not rely on training-data geography.** Street-level east/west/north/south claims are frequently wrong. Always derive from coordinates.

---

## Step 1 — Geocode All Locations

Use **Nominatim** (no API key required, 1 req/sec rate limit):

```python
import urllib.request, urllib.parse, json, time, math

def geocode(place: str, region: str = "Seattle, WA") -> tuple[float, float, str] | None:
    params = urllib.parse.urlencode({"q": f"{place}, {region}", "format": "json", "limit": 1})
    req = urllib.request.Request(
        f"https://nominatim.openstreetmap.org/search?{params}",
        headers={"User-Agent": "GeoValidation/1.0 contact@example.com"},
    )
    with urllib.request.urlopen(req, timeout=10) as r:
        results = json.loads(r.read())
    time.sleep(1.1)  # Nominatim policy: max 1 req/sec
    if results:
        return float(results[0]["lat"]), float(results[0]["lon"]), results[0]["display_name"]
    return None
```

**Always geocode both the origin and destination before making any directional claim.**

For named venues (bars, parks, hotels) that Nominatim may not find by address alone, try the venue name first, then fall back to the street address.

---

## Step 2 — Compute Cardinal Direction

```python
def bearing(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
    """True bearing from A to B, degrees clockwise from north."""
    φ1, φ2 = math.radians(lat1), math.radians(lat2)
    Δλ = math.radians(lon2 - lon1)
    x = math.sin(Δλ) * math.cos(φ2)
    y = math.cos(φ1) * math.sin(φ2) - math.sin(φ1) * math.cos(φ2) * math.cos(Δλ)
    return (math.degrees(math.atan2(x, y)) + 360) % 360

def cardinal(brng: float, precision: str = "simple") -> str:
    """
    precision='simple'  → N / S / E / W
    precision='ordinal' → N / NE / E / SE / S / SW / W / NW
    """
    if precision == "simple":
        dirs = ["N", "E", "S", "W"]
        return dirs[round(brng / 90) % 4]
    dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
    return dirs[round(brng / 45) % 8]

def haversine_miles(lat1, lon1, lat2, lon2) -> float:
    R = 3958.8
    φ1, φ2 = math.radians(lat1), math.radians(lat2)
    dφ = math.radians(lat2 - lat1)
    dλ = math.radians(lon2 - lon1)
    a = math.sin(dφ/2)**2 + math.cos(φ1) * math.cos(φ2) * math.sin(dλ/2)**2
    return R * 2 * math.asin(math.sqrt(a))
```

Use `cardinal(bearing(lat_a, lon_a, lat_b, lon_b))` to verify any "B is east/west of A" claim before writing it. **Do not use longitude comparison alone** — longitude increases eastward, but street grids don't always align with cardinal axes.

---

## Step 3 — Detect Road Barriers Between Two Points

Before stating a walk is feasible, check whether a major road lies between the two points. Query Overpass for high-speed roads in the bounding box:

```python
OVERPASS_URL = "https://overpass-api.de/api/interpreter"

def find_barriers(lat_a, lon_a, lat_b, lon_b) -> list[dict]:
    """
    Returns major roads (motorway, trunk, primary) in the bounding box between A and B.
    These are potential pedestrian barriers requiring crossing analysis.
    """
    s = min(lat_a, lat_b) - 0.002
    n = max(lat_a, lat_b) + 0.002
    w = min(lon_a, lon_b) - 0.003
    e = max(lon_a, lon_b) + 0.003
    query = f"""
[out:json][timeout:25];
way["highway"~"motorway|trunk|primary"]["name"]({s},{w},{n},{e});
out body;
"""
    data = urllib.parse.urlencode({"data": query}).encode()
    req = urllib.request.Request(OVERPASS_URL, data=data, headers={"User-Agent": "GeoValidation/1.0"})
    with urllib.request.urlopen(req, timeout=30) as r:
        elements = json.loads(r.read())["elements"]
    return [{"name": el["tags"].get("name",""), "highway": el["tags"].get("highway","")} for el in elements]
```

If `find_barriers()` returns results, proceed to Step 4 to find safe crossing points.

---

## Step 4 — Find Pedestrian Crossings on Barrier Roads

```python
def find_crossings_on_road(road_name: str, lat_a, lon_a, lat_b, lon_b) -> list[dict]:
    """
    Find signalized pedestrian crossings on a named road between two bounding points.
    Returns crossings sorted south-to-north by latitude.
    """
    s = min(lat_a, lat_b) - 0.005
    n = max(lat_a, lat_b) + 0.005
    w = min(lon_a, lon_b) - 0.008
    e = max(lon_a, lon_b) + 0.008

    # First: get way node IDs for the named road
    way_query = f"""
[out:json][timeout:25];
way["name"="{road_name}"]["highway"]({s},{w},{n},{e});
out body;
>;
out skel qt;
"""
    data = urllib.parse.urlencode({"data": way_query}).encode()
    req = urllib.request.Request(OVERPASS_URL, data=data, headers={"User-Agent": "GeoValidation/1.0"})
    with urllib.request.urlopen(req, timeout=30) as r:
        result = json.loads(r.read())

    road_node_ids = set()
    for el in result["elements"]:
        if el["type"] == "way":
            road_node_ids.update(el.get("nodes", []))

    # Second: get all crossing nodes in area, filter to those on the road
    cross_query = f"""
[out:json][timeout:25];
(
  node["highway"="crossing"]({s},{w},{n},{e});
  node["highway"="traffic_signals"]({s},{w},{n},{e});
);
out body;
"""
    data = urllib.parse.urlencode({"data": cross_query}).encode()
    req = urllib.request.Request(OVERPASS_URL, data=data, headers={"User-Agent": "GeoValidation/1.0"})
    with urllib.request.urlopen(req, timeout=30) as r:
        nodes = json.loads(r.read())["elements"]

    crossings = []
    seen_lats = set()
    for node in nodes:
        if node["id"] not in road_node_ids:
            continue
        lat = round(node["lat"], 4)
        if lat in seen_lats:
            continue
        seen_lats.add(lat)
        tags = node.get("tags", {})
        crossings.append({
            "lat": node["lat"],
            "lon": node["lon"],
            "type": tags.get("crossing", tags.get("highway", "")),
            "signals": tags.get("crossing", "") == "traffic_signals" or tags.get("button_operated") == "yes",
        })

    return sorted(crossings, key=lambda c: c["lat"])
```

---

## Step 5 — Compute Realistic Walk Time

Straight-line distance understates real walk time when barriers exist. Apply:

```python
def walk_time_minutes(
    lat_a, lon_a, lat_b, lon_b,
    barrier_detour_miles: float = 0.0,
    pace_min_per_mile: float = 20,
) -> int:
    """
    pace_min_per_mile: 20 = city walk (with lights), 15 = brisk, 25 = leisurely
    barrier_detour_miles: extra distance to reach a crossing and backtrack
    """
    straight = haversine_miles(lat_a, lon_a, lat_b, lon_b)
    city_factor = 1.2  # grid detour vs. straight line
    total = (straight * city_factor + barrier_detour_miles) * pace_min_per_mile
    return round(total)
```

**Barrier detour estimation:**
- Pedestrian must walk to nearest crossing, cross, then walk back toward destination
- Estimate: `distance_to_nearest_crossing + crossing_width (~0.05 mi)`
- If no signalized crossing exists within 0.5 miles of the direct path: **do not state a walk time** — state "not recommended on foot; use rideshare or bus"

---

## Step 6 — Local OSM Subset (for Offline / High-Volume Use)

When running many queries or Overpass times out repeatedly, download a regional PBF:

```bash
# Install osmium-tool
brew install osmium-tool

# Download Washington State extract from Geofabrik (~300MB)
curl -O https://download.geofabrik.de/north-america/us/washington-latest.osm.pbf

# Extract just the Seattle area bounding box (lat 47.48-47.78, lon -122.46 to -122.22)
osmium extract \
  --bbox -122.46,47.48,-122.22,47.78 \
  washington-latest.osm.pbf \
  -o seattle.osm.pbf

# Convert to GeoJSON for Python use
osmium export seattle.osm.pbf -o seattle.geojson

# Query locally instead of Overpass — eliminates timeouts
```

For Python queries against a local PBF, install `pyrosm`:
```bash
uv add pyrosm   # or pip install pyrosm
```

```python
from pyrosm import OSM
osm = OSM("seattle.osm.pbf")
roads = osm.get_network("driving")  # or "walking", "cycling"
pois = osm.get_pois(custom_filter={"tourism": ["hotel", "motel"]})
```

Cache the downloaded PBF in the project directory. Geofabrik updates daily; re-download when data feels stale (> 2 weeks old for active urban areas).

---

## Validation Checklist

Before finalizing any content with directional or routing claims:

- [ ] All named locations geocoded via Nominatim — lat/lon confirmed, not assumed
- [ ] Cardinal direction derived from `bearing()` — not from memory or intuition
- [ ] Walk time uses haversine + city_factor (1.2), not straight-line
- [ ] Major road barriers checked via `find_barriers()`
- [ ] If barrier found: crossing locations confirmed via `find_crossings_on_road()`
- [ ] If no crossings within 0.5 mi of route: walking flagged as not recommended
- [ ] "Walk X min" claims account for detour to crossing, not just destination distance
- [ ] No directional phrases written before coordinates are in hand

---

## Common Failure Modes to Watch For

| Claim | Why it fails | Fix |
|---|---|---|
| "walk east to X" | Longitude alone doesn't determine east/west — use bearing() | Geocode both points, compute cardinal |
| "X is a 10-min walk" | Ignores barriers, grid detour factor, and crossing detours | Use walk_time_minutes() with barrier check |
| "cross Aurora at the light" | Assumes crossings exist — may be a mile gap | Query find_crossings_on_road() first |
| "Gas Works Park is west of Y" | Named parks are hard to intuit — their centroid may surprise you | Always geocode the park itself |
| "walk along the waterfront to X" | Waterways, fences, and grade changes break apparent routes | Query for barriers, not just distance |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…