#!/usr/bin/env python3 """Report streets, boundary intersections and the demo building set for an OSM extract. Usage: Geo/scripts/block_report.py Geo/raw/osm-api-map-2026-11-06.osm \ --blvd "Hollywood Boulevard" --west "North Orange Drive" --east "North Highland Avenue" Reads a plain .osm XML file (the output of the OSM API `map` endpoint or an Overpass export). No network access, no dependencies beyond the standard library. Output: the boulevard's block span in metres, the two boundary intersections, the buildings on the boulevard frontage split by side (even/odd is not trusted — latitude relative to the centreline is), and each building's distance from the centreline. Buildings behind the frontage are listed but marked as off-buffer. """ import argparse import math import xml.etree.ElementTree as ET EARTH_R = 6378137.0 def local_metres(lat, lon, lat0, lon0): x = math.radians(lon - lon0) * EARTH_R * math.cos(math.radians(lat0)) y = math.radians(lat - lat0) * EARTH_R return x, y def load(path): root = ET.parse(path).getroot() nodes = {n.get("id"): (float(n.get("lat")), float(n.get("lon"))) for n in root.findall("node")} ways = [] for w in root.findall("way"): tags = {c.get("k"): c.get("v") for c in w.findall("tag")} ways.append((w.get("id"), tags, [nd.get("ref") for nd in w.findall("nd")])) return nodes, ways def refs_by_name(ways, name): return {ref for _, tags, refs in ways if tags.get("name") == name for ref in refs} def main(): ap = argparse.ArgumentParser() ap.add_argument("osm") ap.add_argument("--blvd", default="Hollywood Boulevard") ap.add_argument("--west", default="North Orange Drive") ap.add_argument("--east", default="North Highland Avenue") ap.add_argument("--buffer", type=float, default=50.0, help="north-side buffer in metres, from the centreline") args = ap.parse_args() nodes, ways = load(args.osm) blvd = refs_by_name(ways, args.blvd) if not blvd: raise SystemExit(f"no highway named {args.blvd!r} in {args.osm}") ends = {} for label, street in (("west", args.west), ("east", args.east)): inter = refs_by_name(ways, street) & blvd pts = sorted(nodes[i][1] for i in inter if i in nodes) if not pts: raise SystemExit(f"no intersection between {args.blvd!r} and {street!r}") ends[label] = pts[0] if label == "west" else pts[-1] print(f"{street} x {args.blvd}: {len(pts)} shared node(s), lon {pts[0]:.6f}..{pts[-1]:.6f}") lo_min, lo_max = ends["west"], ends["east"] lat0 = sum(nodes[i][0] for i in blvd if lo_min <= nodes[i][1] <= lo_max) / max( 1, len([i for i in blvd if lo_min <= nodes[i][1] <= lo_max]) ) span = math.radians(lo_max - lo_min) * EARTH_R * math.cos(math.radians(lat0)) print(f"block span: {span:.1f} m of frontage, centreline lat {lat0:.6f}\n") rows = [] for wid, tags, refs in ways: if "building" not in tags: continue pts = [nodes[r] for r in refs if r in nodes] if not pts: continue lat = sum(p[0] for p in pts) / len(pts) lon = sum(p[1] for p in pts) / len(pts) if not (lo_min - 0.00005 <= lon <= lo_max + 0.00005): continue side = "S" if lat < lat0 else "N" _, y = local_metres(lat, lon, lat0, lon) rows.append((side, abs(y), lon, wid, tags)) rows.sort(key=lambda r: r[2]) for side, dist, lon, wid, tags in rows: if side == "S": flag = "" else: flag = "" if dist <= args.buffer else " (outside buffer)" print( f"{side} way {wid:>10} {tags.get('addr:housenumber', '-'):>11} " f"{tags.get('addr:street', '')[:22]:22} h={tags.get('height', '-'):>5} " f"lv={tags.get('building:levels', '-'):>3} d={dist:5.1f}m {tags.get('name', '')[:30]}{flag}" ) if __name__ == "__main__": main()