diff --git a/exposure/__init__.py b/exposure/__init__.py index a2f1bbe..6ff2cce 100644 --- a/exposure/__init__.py +++ b/exposure/__init__.py @@ -1,11 +1,16 @@ """Exposure module: data sources answering 'what assets are at risk where'.""" from .ghsl import get_ghsl_built -from .models import ExposureGrid +from .models import BuildingCollection, BuildingFootprint, ExposureGrid +from .osm import get_osm_buildings, parse_overpass_response from .worldpop import get_worldpop_population __all__ = [ + "BuildingCollection", + "BuildingFootprint", "ExposureGrid", "get_ghsl_built", + "get_osm_buildings", "get_worldpop_population", + "parse_overpass_response", ] diff --git a/exposure/constants.py b/exposure/constants.py index 7ec5cf4..d363311 100644 --- a/exposure/constants.py +++ b/exposure/constants.py @@ -52,3 +52,10 @@ Path.home() / ".cache" / "flood_inundation" / "worldpop" ) + +# --- OpenStreetMap building footprints via Overpass API -------------------- + +OVERPASS_URL: str = "https://overpass-api.de/api/interpreter" +OSM_BUFFER_DEG: float = 0.1 +OSM_DEFAULT_TIMEOUT_S: int = 60 +OSM_USER_AGENT: str = "flood_inundation/0.1 (research; geospatial pipeline)" diff --git a/exposure/models.py b/exposure/models.py index 8c94c44..80229cb 100644 --- a/exposure/models.py +++ b/exposure/models.py @@ -1,6 +1,7 @@ """Dataclasses for the exposure module.""" -from dataclasses import dataclass +from dataclasses import dataclass, field +from types import MappingProxyType import numpy as np from rasterio.transform import Affine @@ -29,3 +30,36 @@ class ExposureGrid: def shape(self) -> tuple[int, int]: """Return the (rows, cols) shape of the underlying raster.""" return self.values.shape # type: ignore[return-value] + + +@dataclass(frozen=True) +class BuildingFootprint: + """One OSM building polygon with metadata. + + `polygon` is an immutable tuple of (lat, lon) vertices in input order + (the closing duplicate may or may not be present). `area_m2` is the + polygon's planar area projected with a cos(lat) factor at the centroid + latitude — accurate to within a fraction of a percent for typical + building-sized polygons. `tags` is a read-only view of the OSM tag dict + so the dataclass stays hashable. + """ + + osm_id: int + polygon: tuple[tuple[float, float], ...] + area_m2: float + tags: MappingProxyType # type: ignore[type-arg] + + +@dataclass +class BuildingCollection: + """A bbox-bounded collection of OSM buildings.""" + + buildings: tuple[BuildingFootprint, ...] = field(default_factory=tuple) + bbox: BoundingBox | None = None + + def __len__(self) -> int: + return len(self.buildings) + + def total_area_m2(self) -> float: + """Sum of footprint area across all buildings.""" + return float(sum(b.area_m2 for b in self.buildings)) diff --git a/exposure/osm.py b/exposure/osm.py new file mode 100644 index 0000000..87e5758 --- /dev/null +++ b/exposure/osm.py @@ -0,0 +1,149 @@ +"""Fetch OSM building footprints via the Overpass API.""" + +import json +import math +import urllib.error +import urllib.request +from collections.abc import Iterable +from types import MappingProxyType + +from dem.models import BoundingBox +from dem.utils import bbox_from_point + +from .constants import ( + OSM_BUFFER_DEG, + OSM_DEFAULT_TIMEOUT_S, + OSM_USER_AGENT, + OVERPASS_URL, +) +from .models import BuildingCollection, BuildingFootprint + +# 1 degree of latitude is ~111,320 m at the WGS84 surface; longitude +# converges with cos(lat). Both are exact enough for building-scale +# polygons (the geodesic correction is well below 0.1% at these sizes). +_METRES_PER_DEGREE_LAT: float = 111_320.0 + + +def _build_overpass_query( + bbox: BoundingBox, + timeout_seconds: int, +) -> str: + """Build an Overpass QL query for building ways inside a bbox. + + Returns the geometry of each way so we don't need a second roundtrip + to fetch node coordinates. + """ + south = bbox.min_lat + west = bbox.min_lon + north = bbox.max_lat + east = bbox.max_lon + return ( + f"[out:json][timeout:{timeout_seconds}];" + f'(way["building"]({south},{west},{north},{east}););' + f"out body geom;" + ) + + +def _polygon_area_m2(polygon: Iterable[tuple[float, float]]) -> float: + """Approximate polygon area in m^2 from (lat, lon) vertices. + + Uses the shoelace formula in a local equirectangular projection + centred on the polygon's mean latitude. Building-sized polygons see + sub-0.1% error from this approximation — far below the noise floor of + the OSM source data itself. + """ + pts = list(polygon) + if len(pts) < 3: + return 0.0 + avg_lat = sum(p[0] for p in pts) / len(pts) + metres_per_deg_lon = _METRES_PER_DEGREE_LAT * math.cos(math.radians(avg_lat)) + + area = 0.0 + n = len(pts) + for i in range(n): + lat_i, lon_i = pts[i] + lat_j, lon_j = pts[(i + 1) % n] + x_i = lon_i * metres_per_deg_lon + y_i = lat_i * _METRES_PER_DEGREE_LAT + x_j = lon_j * metres_per_deg_lon + y_j = lat_j * _METRES_PER_DEGREE_LAT + area += x_i * y_j - x_j * y_i + return abs(area) / 2.0 + + +def _element_to_building(element: dict) -> BuildingFootprint | None: + """Convert one Overpass element to a BuildingFootprint, or None if invalid.""" + geometry = element.get("geometry") + if not geometry or len(geometry) < 3: + return None + polygon = tuple((float(g["lat"]), float(g["lon"])) for g in geometry) + return BuildingFootprint( + osm_id=int(element["id"]), + polygon=polygon, + area_m2=_polygon_area_m2(polygon), + tags=MappingProxyType(dict(element.get("tags", {}))), + ) + + +def parse_overpass_response(response_json: dict) -> BuildingCollection: + """Parse a raw Overpass JSON response into a BuildingCollection. + + Exposed as a public helper so test fixtures (saved Overpass responses) + can be loaded without touching the network. + """ + buildings: list[BuildingFootprint] = [] + for element in response_json.get("elements", []): + if element.get("type") != "way": + continue + building = _element_to_building(element) + if building is not None: + buildings.append(building) + return BuildingCollection(buildings=tuple(buildings)) + + +def get_osm_buildings( + lat: float, + lon: float, + buffer_deg: float = OSM_BUFFER_DEG, + timeout_seconds: int = OSM_DEFAULT_TIMEOUT_S, + overpass_url: str = OVERPASS_URL, +) -> BuildingCollection: + """Query Overpass for OSM building polygons in the bbox around a point. + + Note: Overpass-API.de has rate limits; do not call this in tight loops. + For repeated runs over the same area, save the JSON response once and + reload via `parse_overpass_response`. + + Args: + lat: Latitude of the region center, decimal degrees. + lon: Longitude of the region center, decimal degrees. + buffer_deg: Half-width of the square bbox around the point, degrees. + timeout_seconds: Overpass query timeout (server-side). + overpass_url: Override the Overpass endpoint (e.g. for a private + instance). + + Returns: + A BuildingCollection holding every building way the server returned + plus the requested bbox. + + Raises: + urllib.error.HTTPError: Overpass server returned a non-2xx response + (e.g. rate limited, malformed query). + """ + bbox = bbox_from_point(lat, lon, buffer_deg) + query = _build_overpass_query(bbox=bbox, timeout_seconds=timeout_seconds) + + request = urllib.request.Request( + overpass_url, + data=query.encode("utf-8"), + method="POST", + headers={ + "Content-Type": "text/plain; charset=utf-8", + "User-Agent": OSM_USER_AGENT, + }, + ) + with urllib.request.urlopen(request, timeout=timeout_seconds + 10) as response: + payload = json.loads(response.read()) + + collection = parse_overpass_response(payload) + return BuildingCollection(buildings=collection.buildings, bbox=bbox) diff --git a/tests/conftest.py b/tests/conftest.py index 51073d9..f2dee12 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,9 +6,12 @@ import pytest import rasterio +import json + from dem import get_dem from dem.models import DEM, BoundingBox -from exposure.models import ExposureGrid +from exposure import parse_overpass_response +from exposure.models import BuildingCollection, ExposureGrid from hydrology import build_flow_grid, compute_hand, extract_streams from hydrology.models import FlowGrid, Hand, StreamNetwork @@ -19,6 +22,9 @@ ROBIT_BATA_WORLDPOP_FIXTURE: Path = ( Path(__file__).resolve().parent / "fixtures" / "robit_bata_worldpop.tif" ) +ROBIT_BATA_OSM_FIXTURE: Path = ( + Path(__file__).resolve().parent / "fixtures" / "robit_bata_osm.json" +) ROBIT_BATA_FIXTURE: Path = Path(__file__).resolve().parent / "fixtures" / "robit_bata.tif" @@ -138,6 +144,19 @@ def robit_bata_worldpop() -> ExposureGrid: ) +@pytest.fixture(scope="session") +def robit_bata_osm() -> BuildingCollection: + """Parse the committed Robit Bata OSM Overpass response into buildings. + + Saved as raw JSON so the fixture exercises `parse_overpass_response` + end to end. Regenerate via + `python tests/fixtures/_generate_robit_bata_osm.py`. + """ + with open(ROBIT_BATA_OSM_FIXTURE) as f: + payload = json.load(f) + return parse_overpass_response(payload) + + @pytest.fixture(scope="session") def ne_brazil_dem() -> DEM: """Live DEM at (-5.0, -39.0) with the default buffer. diff --git a/tests/exposure/test_osm.py b/tests/exposure/test_osm.py new file mode 100644 index 0000000..5e845dd --- /dev/null +++ b/tests/exposure/test_osm.py @@ -0,0 +1,120 @@ +"""Tests for exposure.osm (offline using committed Overpass JSON fixture).""" + +import math + +import pytest + +from exposure import get_osm_buildings, parse_overpass_response +from exposure.models import BuildingCollection, BuildingFootprint +from exposure.osm import _polygon_area_m2 + + +class TestPolygonAreaApproximation: + def test_unit_square_at_equator(self) -> None: + # 0.001 deg square at the equator -> ~111.32 m on a side, ~12_392 m^2. + polygon = [(0.0, 0.0), (0.0, 0.001), (0.001, 0.001), (0.001, 0.0)] + area = _polygon_area_m2(polygon) + # Allow 0.5% tolerance for the cos(lat) approximation at the centroid. + assert area == pytest.approx(12392.5, rel=0.005) + + def test_polygon_at_robit_latitude(self) -> None: + # 0.001 deg square at lat=11.8: lon shrinks by cos(11.8 deg) ~ 0.9788. + # Side lengths ~ 111.32 x 108.96; area ~ 12,131 m^2. + polygon = [ + (11.8, 37.5), + (11.8, 37.501), + (11.801, 37.501), + (11.801, 37.5), + ] + area = _polygon_area_m2(polygon) + expected = 111.32 * (111.32 * math.cos(math.radians(11.8005))) + assert area == pytest.approx(expected, rel=0.005) + + def test_degenerate_polygon_returns_zero(self) -> None: + assert _polygon_area_m2([(0.0, 0.0), (0.0, 1.0)]) == 0.0 + + +class TestParseOverpassResponse: + def test_empty_response(self) -> None: + collection = parse_overpass_response({"elements": []}) + assert len(collection) == 0 + + def test_skips_non_way_elements(self) -> None: + payload = { + "elements": [ + {"type": "node", "id": 1, "lat": 0, "lon": 0}, + {"type": "relation", "id": 2, "tags": {"building": "yes"}}, + ] + } + collection = parse_overpass_response(payload) + assert len(collection) == 0 + + def test_skips_ways_with_too_few_vertices(self) -> None: + payload = { + "elements": [ + { + "type": "way", + "id": 99, + "tags": {"building": "house"}, + "geometry": [{"lat": 0, "lon": 0}, {"lat": 0, "lon": 1}], + } + ] + } + collection = parse_overpass_response(payload) + assert len(collection) == 0 + + +class TestRobitBataOsmFixture: + """Use the committed Overpass JSON fixture to assert the parser.""" + + def test_total_building_count( + self, + robit_bata_osm: BuildingCollection, + ) -> None: + # Pinned: 35 buildings in the Robit Bata bbox at fixture-generation time. + assert len(robit_bata_osm) == 35 + + def test_total_area_m2( + self, + robit_bata_osm: BuildingCollection, + ) -> None: + # Pinned to the fixture: ~4,129 m^2 of OSM-mapped building footprints. + assert robit_bata_osm.total_area_m2() == pytest.approx(4129.2, abs=1.0) + + def test_largest_building_is_place_of_worship( + self, + robit_bata_osm: BuildingCollection, + ) -> None: + # Pinned: the largest mapped building in the patch is a place of + # worship at ~599 m^2 (church). + largest = max(robit_bata_osm.buildings, key=lambda b: b.area_m2) + assert largest.area_m2 == pytest.approx(598.9, abs=1.0) + assert largest.tags.get("amenity") == "place_of_worship" + + def test_each_building_has_polygon_and_tags( + self, + robit_bata_osm: BuildingCollection, + ) -> None: + for b in robit_bata_osm.buildings: + assert isinstance(b, BuildingFootprint) + assert len(b.polygon) >= 3 + assert b.area_m2 > 0 + assert "building" in b.tags + + +@pytest.mark.integration +class TestGetOsmBuildingsIntegration: + def test_live_query_returns_buildings(self) -> None: + # Live Overpass query for a tiny well-mapped urban area: central + # Liechtenstein (Vaduz). Uses a small bbox to keep response size low. + collection = get_osm_buildings( + lat=47.141, + lon=9.521, + buffer_deg=0.005, + timeout_seconds=30, + ) + # Vaduz core has at least a few dozen mapped buildings. + assert len(collection) > 10 + for b in collection.buildings: + assert b.area_m2 > 0 + assert "building" in b.tags diff --git a/tests/fixtures/_generate_robit_bata_osm.py b/tests/fixtures/_generate_robit_bata_osm.py new file mode 100644 index 0000000..62c2518 --- /dev/null +++ b/tests/fixtures/_generate_robit_bata_osm.py @@ -0,0 +1,56 @@ +"""Regenerate the Robit Bata OSM buildings test fixture. + +Run this when OSM data for the area is updated and we want the fixture +to follow: + + python tests/fixtures/_generate_robit_bata_osm.py + +The fixture is the raw Overpass JSON response (no post-processing) so +tests can exercise both `parse_overpass_response` and `get_osm_buildings` +deterministically. +""" + +import json +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +import urllib.request + +from dem.utils import bbox_from_point +from exposure.constants import OSM_USER_AGENT, OVERPASS_URL +from exposure.osm import _build_overpass_query + +FIXTURE_PATH: Path = Path(__file__).resolve().parent / "robit_bata_osm.json" +FIXTURE_LAT: float = 11.805 +FIXTURE_LON: float = 37.5625 +FIXTURE_BUFFER_DEG: float = 0.0375 + + +def regenerate() -> None: + """Hit Overpass for the Robit Bata bbox and save the raw JSON to disk.""" + bbox = bbox_from_point(FIXTURE_LAT, FIXTURE_LON, FIXTURE_BUFFER_DEG) + query = _build_overpass_query(bbox=bbox, timeout_seconds=60) + request = urllib.request.Request( + OVERPASS_URL, + data=query.encode("utf-8"), + method="POST", + headers={ + "Content-Type": "text/plain; charset=utf-8", + "User-Agent": OSM_USER_AGENT, + }, + ) + with urllib.request.urlopen(request, timeout=70) as response: + payload = json.loads(response.read()) + + FIXTURE_PATH.write_text(json.dumps(payload, indent=2, sort_keys=True)) + n = len(payload.get("elements", [])) + size_kb = FIXTURE_PATH.stat().st_size / 1024 + print(f"Wrote {FIXTURE_PATH} elements={n} size={size_kb:.1f} KB") + + +if __name__ == "__main__": + regenerate() diff --git a/tests/fixtures/robit_bata_osm.json b/tests/fixtures/robit_bata_osm.json new file mode 100644 index 0000000..70d35ce --- /dev/null +++ b/tests/fixtures/robit_bata_osm.json @@ -0,0 +1,1712 @@ +{ + "elements": [ + { + "bounds": { + "maxlat": 11.7810385, + "maxlon": 37.5527435, + "minlat": 11.7809265, + "minlon": 37.5526433 + }, + "geometry": [ + { + "lat": 11.7810385, + "lon": 37.5526848 + }, + { + "lat": 11.7810066, + "lon": 37.5527435 + }, + { + "lat": 11.7809265, + "lon": 37.5527046 + }, + { + "lat": 11.7809656, + "lon": 37.5526433 + }, + { + "lat": 11.7810385, + "lon": 37.5526848 + } + ], + "id": 928166520, + "nodes": [ + 8610032661, + 8610032662, + 8610032663, + 8610032664, + 8610032661 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7803932, + "maxlon": 37.5524522, + "minlat": 11.7802462, + "minlon": 37.5523184 + }, + "geometry": [ + { + "lat": 11.7803932, + "lon": 37.5524072 + }, + { + "lat": 11.7803574, + "lon": 37.5524522 + }, + { + "lat": 11.7802462, + "lon": 37.55237 + }, + { + "lat": 11.7802898, + "lon": 37.5523184 + }, + { + "lat": 11.7803932, + "lon": 37.5524072 + } + ], + "id": 928167480, + "nodes": [ + 8610104647, + 8610104648, + 8610104649, + 8610104650, + 8610104647 + ], + "tags": { + "building": "house", + "name": "Casa" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7805284, + "maxlon": 37.5521159, + "minlat": 11.7804224, + "minlon": 37.5519687 + }, + "geometry": [ + { + "lat": 11.7804785, + "lon": 37.5519687 + }, + { + "lat": 11.7805284, + "lon": 37.5520156 + }, + { + "lat": 11.7804795, + "lon": 37.5521159 + }, + { + "lat": 11.7804224, + "lon": 37.55208 + }, + { + "lat": 11.7804785, + "lon": 37.5519687 + } + ], + "id": 928167721, + "nodes": [ + 8610093171, + 8610093172, + 8610093173, + 8610093174, + 8610093171 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7804421, + "maxlon": 37.5519955, + "minlat": 11.7803617, + "minlon": 37.551912 + }, + "geometry": [ + { + "lat": 11.780403, + "lon": 37.551912 + }, + { + "lat": 11.7804421, + "lon": 37.5519429 + }, + { + "lat": 11.7804024, + "lon": 37.5519955 + }, + { + "lat": 11.7803617, + "lon": 37.5519724 + }, + { + "lat": 11.780403, + "lon": 37.551912 + } + ], + "id": 928167867, + "nodes": [ + 8610032676, + 8610032677, + 8610032678, + 8610032679, + 8610032676 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.781281, + "maxlon": 37.5524087, + "minlat": 11.7811379, + "minlon": 37.5523082 + }, + "geometry": [ + { + "lat": 11.781281, + "lon": 37.5523551 + }, + { + "lat": 11.7812561, + "lon": 37.5524087 + }, + { + "lat": 11.7811379, + "lon": 37.5523658 + }, + { + "lat": 11.7811668, + "lon": 37.5523082 + }, + { + "lat": 11.781281, + "lon": 37.5523551 + } + ], + "id": 928168138, + "nodes": [ + 8610104309, + 8610104310, + 8610104311, + 8610104312, + 8610104309 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7814412, + "maxlon": 37.5519648, + "minlat": 11.7813729, + "minlon": 37.5519005 + }, + "geometry": [ + { + "lat": 11.7814412, + "lon": 37.5519045 + }, + { + "lat": 11.7814333, + "lon": 37.5519648 + }, + { + "lat": 11.7813729, + "lon": 37.5519608 + }, + { + "lat": 11.7813782, + "lon": 37.5519005 + }, + { + "lat": 11.7814412, + "lon": 37.5519045 + } + ], + "id": 928168455, + "nodes": [ + 8610058582, + 8610058583, + 8610058584, + 8610058585, + 8610058582 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7812889, + "maxlon": 37.5518576, + "minlat": 11.7812022, + "minlon": 37.5517825 + }, + "geometry": [ + { + "lat": 11.7812889, + "lon": 37.5518093 + }, + { + "lat": 11.7812587, + "lon": 37.5518576 + }, + { + "lat": 11.7812022, + "lon": 37.5518348 + }, + { + "lat": 11.7812324, + "lon": 37.5517825 + }, + { + "lat": 11.7812889, + "lon": 37.5518093 + } + ], + "id": 928168725, + "nodes": [ + 8610089154, + 8610089155, + 8610089156, + 8610089157, + 8610089154 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.780855, + "maxlon": 37.5539406, + "minlat": 11.7807716, + "minlon": 37.5538521 + }, + "geometry": [ + { + "lat": 11.780855, + "lon": 37.5538856 + }, + { + "lat": 11.780815, + "lon": 37.5539406 + }, + { + "lat": 11.7807716, + "lon": 37.5539098 + }, + { + "lat": 11.780815, + "lon": 37.5538521 + }, + { + "lat": 11.780855, + "lon": 37.5538856 + } + ], + "id": 928168970, + "nodes": [ + 8610082396, + 8610082397, + 8610082398, + 8610082399, + 8610082396 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7806259, + "maxlon": 37.5541351, + "minlat": 11.7805314, + "minlon": 37.5540539 + }, + "geometry": [ + { + "lat": 11.7806259, + "lon": 37.5540721 + }, + { + "lat": 11.7806016, + "lon": 37.5541351 + }, + { + "lat": 11.7805314, + "lon": 37.5541223 + }, + { + "lat": 11.7805511, + "lon": 37.5540539 + }, + { + "lat": 11.7806259, + "lon": 37.5540721 + } + ], + "id": 928169174, + "nodes": [ + 8610107326, + 8610107327, + 8610107328, + 8610107329, + 8610107326 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7804309, + "maxlon": 37.5542437, + "minlat": 11.7803404, + "minlon": 37.5541612 + }, + "geometry": [ + { + "lat": 11.7804309, + "lon": 37.5541834 + }, + { + "lat": 11.7804021, + "lon": 37.5542437 + }, + { + "lat": 11.7803404, + "lon": 37.5542169 + }, + { + "lat": 11.7803719, + "lon": 37.5541612 + }, + { + "lat": 11.7804309, + "lon": 37.5541834 + } + ], + "id": 928169451, + "nodes": [ + 8610109427, + 8610109428, + 8610109429, + 8610109430, + 8610109427 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7804881, + "maxlon": 37.5540573, + "minlat": 11.7804165, + "minlon": 37.5539802 + }, + "geometry": [ + { + "lat": 11.7804881, + "lon": 37.553999 + }, + { + "lat": 11.7804644, + "lon": 37.5540573 + }, + { + "lat": 11.7804165, + "lon": 37.5540412 + }, + { + "lat": 11.7804414, + "lon": 37.5539802 + }, + { + "lat": 11.7804881, + "lon": 37.553999 + } + ], + "id": 928169891, + "nodes": [ + 8610109353, + 8610109354, + 8610109355, + 8610109356, + 8610109353 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7807716, + "maxlon": 37.5531899, + "minlat": 11.7806981, + "minlon": 37.5531256 + }, + "geometry": [ + { + "lat": 11.7807716, + "lon": 37.5531296 + }, + { + "lat": 11.780769, + "lon": 37.5531899 + }, + { + "lat": 11.7806981, + "lon": 37.5531899 + }, + { + "lat": 11.7807034, + "lon": 37.5531256 + }, + { + "lat": 11.7807716, + "lon": 37.5531296 + } + ], + "id": 928171615, + "nodes": [ + 8610125729, + 8610125730, + 8610125731, + 8610125732, + 8610125729 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7809607, + "maxlon": 37.5536325, + "minlat": 11.780836, + "minlon": 37.5535064 + }, + "geometry": [ + { + "lat": 11.7809108, + "lon": 37.5535064 + }, + { + "lat": 11.7809607, + "lon": 37.5535467 + }, + { + "lat": 11.7808858, + "lon": 37.5536325 + }, + { + "lat": 11.780836, + "lon": 37.5535909 + }, + { + "lat": 11.7809108, + "lon": 37.5535064 + } + ], + "id": 928171962, + "nodes": [ + 8610125561, + 8610125562, + 8610125563, + 8610125564, + 8610125561 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7807139, + "maxlon": 37.5538605, + "minlat": 11.7806036, + "minlon": 37.553776 + }, + "geometry": [ + { + "lat": 11.7807125, + "lon": 37.553776 + }, + { + "lat": 11.7807139, + "lon": 37.553839 + }, + { + "lat": 11.7806193, + "lon": 37.5538605 + }, + { + "lat": 11.7806036, + "lon": 37.5537975 + }, + { + "lat": 11.7807125, + "lon": 37.553776 + } + ], + "id": 928172154, + "nodes": [ + 8610131755, + 8610131756, + 8610131757, + 8610131758, + 8610131755 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7809489, + "maxlon": 37.5537478, + "minlat": 11.780857, + "minlon": 37.5536768 + }, + "geometry": [ + { + "lat": 11.7809489, + "lon": 37.5536848 + }, + { + "lat": 11.7809357, + "lon": 37.5537478 + }, + { + "lat": 11.780857, + "lon": 37.5537411 + }, + { + "lat": 11.7808648, + "lon": 37.5536768 + }, + { + "lat": 11.7809489, + "lon": 37.5536848 + } + ], + "id": 928172703, + "nodes": [ + 8610128620, + 8610128621, + 8610128622, + 8610128623, + 8610128620 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7810828, + "maxlon": 37.5533267, + "minlat": 11.7809974, + "minlon": 37.5532422 + }, + "geometry": [ + { + "lat": 11.7810237, + "lon": 37.5532422 + }, + { + "lat": 11.7810828, + "lon": 37.5532637 + }, + { + "lat": 11.7810513, + "lon": 37.5533267 + }, + { + "lat": 11.7809974, + "lon": 37.5533053 + }, + { + "lat": 11.7810237, + "lon": 37.5532422 + } + ], + "id": 928173047, + "nodes": [ + 8610141919, + 8610141920, + 8610141921, + 8610141922, + 8610141919 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7807349, + "maxlon": 37.5530961, + "minlat": 11.7806522, + "minlon": 37.5530129 + }, + "geometry": [ + { + "lat": 11.7806824, + "lon": 37.5530129 + }, + { + "lat": 11.7807349, + "lon": 37.5530451 + }, + { + "lat": 11.7807073, + "lon": 37.5530961 + }, + { + "lat": 11.7806522, + "lon": 37.5530706 + }, + { + "lat": 11.7806824, + "lon": 37.5530129 + } + ], + "id": 928173175, + "nodes": [ + 8610139754, + 8610139755, + 8610139756, + 8610139757, + 8610139754 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7791791, + "maxlon": 37.5525137, + "minlat": 11.7790354, + "minlon": 37.5523937 + }, + "geometry": [ + { + "lat": 11.7791791, + "lon": 37.5524634 + }, + { + "lat": 11.779147, + "lon": 37.5525137 + }, + { + "lat": 11.7790354, + "lon": 37.5524433 + }, + { + "lat": 11.7790675, + "lon": 37.5523937 + }, + { + "lat": 11.7791791, + "lon": 37.5524634 + } + ], + "id": 928173383, + "nodes": [ + 8610134989, + 8610134990, + 8610134991, + 8610134992, + 8610134989 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7793255, + "maxlon": 37.5527353, + "minlat": 11.7792336, + "minlon": 37.5526387 + }, + "geometry": [ + { + "lat": 11.779273, + "lon": 37.5526387 + }, + { + "lat": 11.7793255, + "lon": 37.5526562 + }, + { + "lat": 11.7792953, + "lon": 37.5527353 + }, + { + "lat": 11.7792336, + "lon": 37.5527179 + }, + { + "lat": 11.779273, + "lon": 37.5526387 + } + ], + "id": 928173486, + "nodes": [ + 8610141928, + 8610141929, + 8610141930, + 8610141931, + 8610141928 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7796918, + "maxlon": 37.5519534, + "minlat": 11.7795159, + "minlon": 37.5518341 + }, + "geometry": [ + { + "lat": 11.7796918, + "lon": 37.551881 + }, + { + "lat": 11.7796655, + "lon": 37.5519534 + }, + { + "lat": 11.7795159, + "lon": 37.5518931 + }, + { + "lat": 11.7795592, + "lon": 37.5518341 + }, + { + "lat": 11.7796918, + "lon": 37.551881 + } + ], + "id": 928173734, + "nodes": [ + 8610151073, + 8610151074, + 8610151075, + 8610151076, + 8610151073 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7801083, + "maxlon": 37.555538, + "minlat": 11.7799386, + "minlon": 37.5554294 + }, + "geometry": [ + { + "lat": 11.7801083, + "lon": 37.5554754 + }, + { + "lat": 11.7800919, + "lon": 37.555538 + }, + { + "lat": 11.7799386, + "lon": 37.5554841 + }, + { + "lat": 11.7799616, + "lon": 37.5554294 + }, + { + "lat": 11.7801083, + "lon": 37.5554754 + } + ], + "id": 928174207, + "nodes": [ + 8610149951, + 8610149952, + 8610149953, + 8610149954, + 8610149951 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.777839, + "maxlon": 37.5431721, + "minlat": 11.7776871, + "minlon": 37.5430567 + }, + "geometry": [ + { + "lat": 11.777839, + "lon": 37.5431073 + }, + { + "lat": 11.7778137, + "lon": 37.5431721 + }, + { + "lat": 11.7776871, + "lon": 37.5431228 + }, + { + "lat": 11.7777209, + "lon": 37.5430567 + }, + { + "lat": 11.777839, + "lon": 37.5431073 + } + ], + "id": 928174817, + "nodes": [ + 8610149566, + 8610149567, + 8610149568, + 8610149569, + 8610149566 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7776319, + "maxlon": 37.5435278, + "minlat": 11.7775161, + "minlon": 37.5434399 + }, + "geometry": [ + { + "lat": 11.7776319, + "lon": 37.5434668 + }, + { + "lat": 11.7776122, + "lon": 37.5435278 + }, + { + "lat": 11.7775161, + "lon": 37.543502 + }, + { + "lat": 11.7775338, + "lon": 37.5434399 + }, + { + "lat": 11.7776319, + "lon": 37.5434668 + } + ], + "id": 928175560, + "nodes": [ + 8610159018, + 8610159019, + 8610159020, + 8610159021, + 8610159018 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7954739, + "maxlon": 37.5541381, + "minlat": 11.7953649, + "minlon": 37.5540389 + }, + "geometry": [ + { + "lat": 11.7954739, + "lon": 37.5540791 + }, + { + "lat": 11.7954358, + "lon": 37.5541381 + }, + { + "lat": 11.7953649, + "lon": 37.5540885 + }, + { + "lat": 11.7954122, + "lon": 37.5540389 + }, + { + "lat": 11.7954739, + "lon": 37.5540791 + } + ], + "id": 928177088, + "nodes": [ + 8610152264, + 8610152265, + 8610152266, + 8610152267, + 8610152264 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7962393, + "maxlon": 37.5534823, + "minlat": 11.7961552, + "minlon": 37.5533978 + }, + "geometry": [ + { + "lat": 11.7962393, + "lon": 37.5534126 + }, + { + "lat": 11.7961671, + "lon": 37.5533978 + }, + { + "lat": 11.7961552, + "lon": 37.5534582 + }, + { + "lat": 11.7962156, + "lon": 37.5534823 + }, + { + "lat": 11.7962393, + "lon": 37.5534126 + } + ], + "id": 928177565, + "nodes": [ + 8610135459, + 8610135460, + 8610135461, + 8610135462, + 8610135459 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7960489, + "maxlon": 37.553662, + "minlat": 11.7959478, + "minlon": 37.5535695 + }, + "geometry": [ + { + "lat": 11.79602, + "lon": 37.5535695 + }, + { + "lat": 11.7960489, + "lon": 37.5536137 + }, + { + "lat": 11.7959938, + "lon": 37.553662 + }, + { + "lat": 11.7959478, + "lon": 37.5536124 + }, + { + "lat": 11.79602, + "lon": 37.5535695 + } + ], + "id": 928177768, + "nodes": [ + 8610176222, + 8610176223, + 8610176224, + 8610176225, + 8610176222 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7961671, + "maxlon": 37.5534126, + "minlat": 11.7960765, + "minlon": 37.553312 + }, + "geometry": [ + { + "lat": 11.7961224, + "lon": 37.553312 + }, + { + "lat": 11.7961671, + "lon": 37.5533401 + }, + { + "lat": 11.7961316, + "lon": 37.5534126 + }, + { + "lat": 11.7960765, + "lon": 37.5533817 + }, + { + "lat": 11.7961224, + "lon": 37.553312 + } + ], + "id": 928177894, + "nodes": [ + 8610193476, + 8610193477, + 8610193478, + 8610193479, + 8610193476 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7960935, + "maxlon": 37.5535722, + "minlat": 11.7960003, + "minlon": 37.553481 + }, + "geometry": [ + { + "lat": 11.7960463, + "lon": 37.553481 + }, + { + "lat": 11.7960935, + "lon": 37.5535145 + }, + { + "lat": 11.7960542, + "lon": 37.5535722 + }, + { + "lat": 11.7960003, + "lon": 37.5535427 + }, + { + "lat": 11.7960463, + "lon": 37.553481 + } + ], + "id": 928178159, + "nodes": [ + 8610155763, + 8610155764, + 8610155765, + 8610155766, + 8610155763 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7957916, + "maxlon": 37.5535722, + "minlat": 11.7957115, + "minlon": 37.5534783 + }, + "geometry": [ + { + "lat": 11.7957325, + "lon": 37.5534783 + }, + { + "lat": 11.7957916, + "lon": 37.5535064 + }, + { + "lat": 11.7957653, + "lon": 37.5535722 + }, + { + "lat": 11.7957115, + "lon": 37.553544 + }, + { + "lat": 11.7957325, + "lon": 37.5534783 + } + ], + "id": 928178239, + "nodes": [ + 8610155682, + 8610155683, + 8610155684, + 8610155685, + 8610155682 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7950945, + "maxlon": 37.5525043, + "minlat": 11.7949777, + "minlon": 37.5523849 + }, + "geometry": [ + { + "lat": 11.7950597, + "lon": 37.5523849 + }, + { + "lat": 11.7950945, + "lon": 37.5524352 + }, + { + "lat": 11.7950157, + "lon": 37.5525043 + }, + { + "lat": 11.7949777, + "lon": 37.5524533 + }, + { + "lat": 11.7950597, + "lon": 37.5523849 + } + ], + "id": 928178923, + "nodes": [ + 8610163653, + 8610163654, + 8610163655, + 8610163656, + 8610163653 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7950282, + "maxlon": 37.5523132, + "minlat": 11.7949324, + "minlon": 37.5522227 + }, + "geometry": [ + { + "lat": 11.7950059, + "lon": 37.5522227 + }, + { + "lat": 11.7950282, + "lon": 37.552279 + }, + { + "lat": 11.7949645, + "lon": 37.5523132 + }, + { + "lat": 11.7949324, + "lon": 37.5522522 + }, + { + "lat": 11.7950059, + "lon": 37.5522227 + } + ], + "id": 928179141, + "nodes": [ + 8610198188, + 8610198189, + 8610198190, + 8610198191, + 8610198188 + ], + "tags": { + "building": "house" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.8003358, + "maxlon": 37.5677661, + "minlat": 11.8001129, + "minlon": 37.5675397 + }, + "geometry": [ + { + "lat": 11.8001203, + "lon": 37.5676122 + }, + { + "lat": 11.8001129, + "lon": 37.5676505 + }, + { + "lat": 11.8001186, + "lon": 37.5676892 + }, + { + "lat": 11.8001368, + "lon": 37.5677236 + }, + { + "lat": 11.8001653, + "lon": 37.5677498 + }, + { + "lat": 11.8002008, + "lon": 37.5677645 + }, + { + "lat": 11.800239, + "lon": 37.5677661 + }, + { + "lat": 11.8002756, + "lon": 37.5677544 + }, + { + "lat": 11.8003061, + "lon": 37.5677308 + }, + { + "lat": 11.800327, + "lon": 37.567698 + }, + { + "lat": 11.8003358, + "lon": 37.5676599 + }, + { + "lat": 11.8003326, + "lon": 37.5676253 + }, + { + "lat": 11.8003194, + "lon": 37.5675933 + }, + { + "lat": 11.8002973, + "lon": 37.5675668 + }, + { + "lat": 11.8002684, + "lon": 37.5675484 + }, + { + "lat": 11.8002355, + "lon": 37.5675397 + }, + { + "lat": 11.8002015, + "lon": 37.5675416 + }, + { + "lat": 11.8001696, + "lon": 37.5675538 + }, + { + "lat": 11.80014, + "lon": 37.5675786 + }, + { + "lat": 11.8001203, + "lon": 37.5676122 + } + ], + "id": 1194237127, + "nodes": [ + 11085370488, + 11085370489, + 11085370490, + 11085370491, + 11085370492, + 11085370493, + 11085370494, + 11085370495, + 11085370496, + 11085370497, + 11085370460, + 11085370482, + 11085370483, + 11085370484, + 11085370485, + 11085370486, + 11085370487, + 11085370461, + 11085370462, + 11085370488 + ], + "tags": { + "amenity": "place_of_worship", + "building": "yes" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7989015, + "maxlon": 37.567006, + "minlat": 11.7986527, + "minlon": 37.5667511 + }, + "geometry": [ + { + "lat": 11.7986527, + "lon": 37.5668968 + }, + { + "lat": 11.7986657, + "lon": 37.5669379 + }, + { + "lat": 11.7986914, + "lon": 37.5669722 + }, + { + "lat": 11.7987268, + "lon": 37.5669958 + }, + { + "lat": 11.7987679, + "lon": 37.567006 + }, + { + "lat": 11.79881, + "lon": 37.5670017 + }, + { + "lat": 11.7988482, + "lon": 37.5669833 + }, + { + "lat": 11.7988783, + "lon": 37.5669529 + }, + { + "lat": 11.7988968, + "lon": 37.5669141 + }, + { + "lat": 11.7989015, + "lon": 37.5668711 + }, + { + "lat": 11.798892, + "lon": 37.5668291 + }, + { + "lat": 11.7988713, + "lon": 37.5667949 + }, + { + "lat": 11.7988412, + "lon": 37.5667689 + }, + { + "lat": 11.7988047, + "lon": 37.5667538 + }, + { + "lat": 11.7987655, + "lon": 37.5667511 + }, + { + "lat": 11.7987273, + "lon": 37.5667609 + }, + { + "lat": 11.798694, + "lon": 37.5667823 + }, + { + "lat": 11.7986689, + "lon": 37.5668133 + }, + { + "lat": 11.7986538, + "lon": 37.5668536 + }, + { + "lat": 11.7986527, + "lon": 37.5668968 + } + ], + "id": 1194237128, + "nodes": [ + 11085370472, + 11085370473, + 11085370474, + 11085370475, + 11085370476, + 11085370477, + 11085370478, + 11085370479, + 11085370480, + 11085370481, + 11085370463, + 11085370466, + 11085370467, + 11085370468, + 11085370469, + 11085370470, + 11085370471, + 11085370464, + 11085370465, + 11085370472 + ], + "tags": { + "amenity": "place_of_worship", + "building": "yes" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.8039686, + "maxlon": 37.5356439, + "minlat": 11.8037566, + "minlon": 37.5354276 + }, + "geometry": [ + { + "lat": 11.803766, + "lon": 37.5355824 + }, + { + "lat": 11.8037856, + "lon": 37.5356116 + }, + { + "lat": 11.8038133, + "lon": 37.5356329 + }, + { + "lat": 11.8038463, + "lon": 37.5356439 + }, + { + "lat": 11.803881, + "lon": 37.5356435 + }, + { + "lat": 11.8039137, + "lon": 37.5356318 + }, + { + "lat": 11.803941, + "lon": 37.5356099 + }, + { + "lat": 11.8039599, + "lon": 37.5355802 + }, + { + "lat": 11.8039686, + "lon": 37.5355459 + }, + { + "lat": 11.803966, + "lon": 37.5355106 + }, + { + "lat": 11.8039524, + "lon": 37.535478 + }, + { + "lat": 11.8039284, + "lon": 37.5354509 + }, + { + "lat": 11.8038971, + "lon": 37.5354335 + }, + { + "lat": 11.8038619, + "lon": 37.5354276 + }, + { + "lat": 11.8038267, + "lon": 37.5354339 + }, + { + "lat": 11.8037956, + "lon": 37.5354517 + }, + { + "lat": 11.8037719, + "lon": 37.5354791 + }, + { + "lat": 11.8037585, + "lon": 37.5355129 + }, + { + "lat": 11.8037566, + "lon": 37.5355482 + }, + { + "lat": 11.803766, + "lon": 37.5355824 + } + ], + "id": 1194254106, + "nodes": [ + 11085515711, + 11085515712, + 11085515713, + 11085515714, + 11085515715, + 11085515716, + 11085515717, + 11085515718, + 11085515719, + 11085515720, + 11085505702, + 11085515705, + 11085515706, + 11085515707, + 11085515708, + 11085515709, + 11085515710, + 11085505703, + 11085505704, + 11085515711 + ], + "tags": { + "amenity": "place_of_worship", + "building": "yes" + }, + "type": "way" + }, + { + "bounds": { + "maxlat": 11.7836218, + "maxlon": 37.567097, + "minlat": 11.783388, + "minlon": 37.5668582 + }, + "geometry": [ + { + "lat": 11.7836218, + "lon": 37.5669061 + }, + { + "lat": 11.7834349, + "lon": 37.5668582 + }, + { + "lat": 11.783388, + "lon": 37.5670491 + }, + { + "lat": 11.7835748, + "lon": 37.567097 + }, + { + "lat": 11.7836218, + "lon": 37.5669061 + } + ], + "id": 1194263515, + "nodes": [ + 11085570029, + 11085570030, + 11085570031, + 11085570032, + 11085570029 + ], + "tags": { + "amenity": "place_of_worship", + "building": "church", + "denomination": "ethiopian_orthodox", + "religion": "christian" + }, + "type": "way" + } + ], + "generator": "Overpass API 0.7.62.11 87bfad18", + "osm3s": { + "copyright": "The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.", + "timestamp_osm_base": "2026-05-04T00:00:14Z" + }, + "version": 0.6 +} \ No newline at end of file