dd-l3-lagos-overture-buildings

Status: done
Score: 100
Duration: 12:48
Cost: 1.29¢
Model: deepseek/deepseek-v4-flash

Map (reference ← swipe → agent)

0:00
Updating the flood-risk model before rainy season. Need every building footprint exceeding 1000 m² across Lagos State (Nigeria) from Overture. Also need a per-LGA roll-up: for each Lagos Local Government Area, the building count, total footprint area, count of buildings with a non-null Overture height, and the median height where available (null otherwise). Write two output files. `lagos_buildings.geoparquet` should be EPSG:4326 GeoParquet with columns `id`, `height`, `footprint_area_m2`, `lga`, and `geometry`. `lagos_building_summary.parquet` should be a plain Parquet with no geometry column and columns `lga`, `n_buildings`, `total_footprint_m2`, `n_with_height`, and `p50_height_m`.
0:00
System info
{
  "adapter": "openrouter",
  "model": "deepseek/deepseek-v4-flash",
  "base_url": "https://openrouter.ai/api/v1"
}
0:16
Let me break this down:

1. Get Overture Maps data for Lagos State, Nigeria
2. Filter to buildings with footprint > 1000 m²
3. For each building: id, height, footprint_area_m2, lga, geometry
4. Per-LGA rollup: building count, total footprint area, count with non-null height, median height
5. Output: `lagos_buildings.geoparquet` (EPSG:4326) and `lagos_building_summary.parquet`

Let me first understand the Lagos State geography. Lagos State is in Nigeria. I need to get the LGA boundaries and the buildings.

Let me use overturemaps to download the building data for Lagos. I'll need to figure out the bounding box for Lagos State, Nigeria.

Actually, let me start by exploring what data is available and figure out the approach.

Lagos State, Nigeria roughly spans from about 2.6°E to 3.8°E, and 6.2°N to 6.7°N. But I should be more precise.

Let me use overturemaps to get the buildings data. The overturemaps library can query by bounding box or by administrative region.

Actually, let me think about this more carefully. Overture Maps has a `places` theme with `division` area type. I could look up the division for Lagos State to get its geometry, then use that to query buildings.

Or I could use a combination approach - use overturemaps to get buildings in the bounding box of Lagos, then do a spatial join with LGA boundaries.

Let me start by writing a script that:
1. Gets Lagos State boundary from Overture divisions
2. Gets buildings for that area
3. Gets LGA boundaries
4. Does the analysis

Actually, Lagos State LGAs - there are 20 LGAs in Lagos State. Let me get the Overture division data for Lagos State and its LGAs.

Let me write a Python script step by step.

First, let me see what data is available from overturemaps.
0:16
Bash cd /work && python -c "import overturemaps; help(overturemaps)" 2>&1 | head -40
Help on package overturemaps:

NAME
    overturemaps

PACKAGE CONTENTS
    __main__
    changelog
    cli
    core
    models
    releases
    state
    writers

FILE
    /usr/local/lib/python3.14/site-packages/overturemaps/__init__.py
0:21
Let me explore how overturemaps works and what functions are available.
0:21
Bash cd /work && python -c "import overturemaps; print(dir(overturemaps))"
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'core', 'geodataframe', 'get_all_overture_types', 'models', 'record_batch_reader']
0:30
Let me explore the overturemaps API more to understand how to query building footprints.
0:30
Bash cd /work && python -c "import overturemaps; help(overturemaps.record_batch_reade...
Help on function record_batch_reader in module overturemaps.core:

record_batch_reader(
    overture_type,
    bbox=None,
    release=None,
    connect_timeout=None,
    request_timeout=None,
    stac=False
) -> Optional[pa.RecordBatchReader]
    Return a pyarrow RecordBatchReader for the desired bounding box and s3 path, or None on error.
0:32
Bash cd /work && python -c "import overturemaps; print(overturemaps.get_all_overture_...
['address', 'bathymetry', 'building', 'building_part', 'division', 'division_area', 'division_boundary', 'place', 'segment', 'connector', 'infrastructure', 'land', 'land_cover', 'land_use', 'water']
0:40
Let me figure out how to use overturemaps to get building data. I need to:

1. Get the bounding box for Lagos State, Nigeria
2. Query Overture for buildings in that area
3. Filter to buildings with footprint area > 1000 m²
4. Join with LGA (Local Government Area) boundaries from Overture's division data
5. Compute the summary statistics per LGA
6. Write both output files

Let me first explore how to use overturemaps.record_batch_reader and the division data.

For Lagos State, I need its geometry. I can get that from Overture's division data.

Let me start by exploring.
0:40
Bash cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow as pa

# Check what the building schema looks like
reader = overturemaps.record_batch_reader('building', bbox=(-180, -90, 180, 90))
if reader:
    schema = reader.schema
    print('Building schema:')
    print(schema)
else:
    print('No reader returned')
" 2>&1 | head -40
Building schema:
id: string
names: struct<primary: string, common: map<string, string ('common')>, rules: list<element: struct<variant: (... 159 chars omitted)
  child 0, primary: string
  child 1, common: map<string, string ('common')>
      child 0, common: struct<key: string not null, value: string> not null
          child 0, key: string not null
          child 1, value: string
  child 2, rules: list<element: struct<variant: string, language: string, perspectives: struct<mode: string, countries (... 87 chars omitted)
      child 0, element: struct<variant: string, language: string, perspectives: struct<mode: string, countries: list<element (... 72 chars omitted)
          child 0, variant: string
          child 1, language: string
          child 2, perspectives: struct<mode: string, countries: list<element: string>>
              child 0, mode: string
              child 1, countries: list<element: string>
                  child 0, element: string
          child 3, value: string
          child 4, between: list<element: double>
              child 0, element: double
          child 5, side: string
sources: list<element: struct<property: string, dataset: string, license: string, record_id: string, update_t (... 65 chars omitted)
  child 0, element: struct<property: string, dataset: string, license: string, record_id: string, update_time: string, c (... 50 chars omitted)
      child 0, property: string
      child 1, dataset: string
      child 2, license: string
      child 3, record_id: string
      child 4, update_time: string
      child 5, confidence: double
      child 6, between: list<element: double>
          child 0, element: double
level: int32
height: double
min_height: double
is_underground: bool
num_floors: int32
num_floors_underground: int32
min_floor: int32
subtype: string
class: string
facade_color: string
1:33
Bash cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow as pa

# Check what the division schema looks like
reader = overturemaps.record_batch_reader('division', bbox=(-180, -90, 180, 90))
if reader:
    schema = reader.schema
    print('Division schema:')
    print(schema)
else:
    print('No reader returned')
" 2>&1 | head -40
Division schema:
id: string
geometry: binary
  -- field metadata --
  ARROW:extension:name: 'geoarrow.wkb'
country: string
sources: list<element: struct<property: string, dataset: string, license: string, record_id: string, update_t (... 65 chars omitted)
  child 0, element: struct<property: string, dataset: string, license: string, record_id: string, update_time: string, c (... 50 chars omitted)
      child 0, property: string
      child 1, dataset: string
      child 2, license: string
      child 3, record_id: string
      child 4, update_time: string
      child 5, confidence: double
      child 6, between: list<element: double>
          child 0, element: double
subtype: string
admin_level: int32
class: string
names: struct<primary: string, common: map<string, string ('common')>, rules: list<element: struct<variant: (... 159 chars omitted)
  child 0, primary: string
  child 1, common: map<string, string ('common')>
      child 0, common: struct<key: string not null, value: string> not null
          child 0, key: string not null
          child 1, value: string
  child 2, rules: list<element: struct<variant: string, language: string, perspectives: struct<mode: string, countries (... 87 chars omitted)
      child 0, element: struct<variant: string, language: string, perspectives: struct<mode: string, countries: list<element (... 72 chars omitted)
          child 0, variant: string
          child 1, language: string
          child 2, perspectives: struct<mode: string, countries: list<element: string>>
              child 0, mode: string
              child 1, countries: list<element: string>
                  child 0, element: string
          child 3, value: string
          child 4, between: list<element: double>
              child 0, element: double
          child 5, side: string
wikidata: string
perspectives: struct<mode: string, countries: list<element: string>>
  child 0, mode: string
1:57
I need to get buildings for Lagos State, Nigeria. Let me understand the workflow:

1. Find the bounding box for Lagos State, Nigeria using the `division` type.
2. Get all buildings within that bounding box (or directly filter for Lagos State).
3. Filter buildings with area > 1000 m².
4. Join with LGA boundaries to assign each building to an LGA.
5. Compute per-LGA summary.

For division data, I see there's a `subtype` field - Lagos State is a `region` and LGAs are `locality`. Actually, let me check what divisions look like for Nigeria.

Let me think about the approach:

1. Get the Lagos State boundary from Overture divisions (admin_level=4 or something appropriate for a state in Nigeria)
2. Get all LGAs in Lagos State from Overture divisions
3. Get all buildings within the Lagos State bounding box
4. Spatial join buildings to LGAs
5. Filter buildings with area > 1000 m²
6. Compute summaries

Actually, let me think about this more carefully. Overture buildings have geometry and height fields. The building footprint area needs to be computed. Let me check what geometry format the buildings are in.

Let me first figure out Nigeria's divisions and then get the Lagos boundary.
1:57
Bash cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow as pa

# Get division data for Nigeria to see admin levels
nigeria_bbox = (2.0, 4.0, 15.0, 14.0)
reader = overturemaps.record_batch_reader('division', bbox=nigeria_bbox)
if reader:
    table = reader.read_all()
    df = table.to_pandas()
    # Filter for Nigeria
    nigeria = df[df['country'] == 'NG']
    print('Columns:', list(nigeria.columns))
    print('admin_level values:', sorted(nigeria['admin_level'].dropna().unique()))
    print('subtype values:', sorted(nigeria['subtype'].dropna().unique()))
    # Look at some rows
    for _, row in nigeria.head(20).iterrows():
        print(row['names'], row['admin_level'], row['subtype'], row['country'])
" 2>&1
Columns: ['id', 'geometry', 'country', 'sources', 'subtype', 'admin_level', 'class', 'names', 'wikidata', 'perspectives', 'local_type', 'region', 'hierarchies', 'parent_division_id', 'norms', 'population', 'capital_division_ids', 'capital_of_divisions', 'cartography', 'version', 'bbox']
admin_level values: [np.float64(0.0), np.float64(1.0), np.float64(2.0)]
subtype values: ['country', 'county', 'locality', 'macrohood', 'microhood', 'neighborhood', 'region']
{'primary': 'Ogidigoi', 'common': None, 'rules': None} nan locality NG
{'primary': 'Ogidigben', 'common': None, 'rules': array([{'variant': 'alternate', 'language': None, 'perspectives': None, 'value': 'Ogidigbe', 'between': None, 'side': None}],
      dtype=object)} nan locality NG
{'primary': 'Akpakpa', 'common': None, 'rules': None} nan locality NG
{'primary': 'Olusumere-Odo', 'common': None, 'rules': None} nan locality NG
{'primary': 'Olusumere-Oke', 'common': None, 'rules': None} nan locality NG
{'primary': 'Okerenkokogbene', 'common': None, 'rules': None} nan locality NG
{'primary': 'Opuede', 'common': None, 'rules': None} nan locality NG
{'primary': 'Arogbogbene', 'common': None, 'rules': None} nan locality NG
{'primary': 'Ufuoma', 'common': None, 'rules': None} nan locality NG
{'primary': 'Tomagbene', 'common': None, 'rules': None} nan locality NG
{'primary': 'Olusumere', 'common': None, 'rules': None} nan locality NG
{'primary': 'Agbobogbene', 'common': None, 'rules': None} nan locality NG
{'primary': 'Sokebolou', 'common': None, 'rules': None} nan locality NG
{'primary': 'Yokri Sobo', 'common': None, 'rules': array([{'variant': 'alternate', 'language': None, 'perspectives': None, 'value': 'Yoki Village', 'between': None, 'side': None}],
      dtype=object)} nan locality NG
{'primary': 'Beniboye', 'common': None, 'rules': None} nan locality NG
{'primary': 'Gulani', 'common': None, 'rules': array([{'variant': 'alternate', 'language': None, 'perspectives': None, 'value': 'Gula', 'between': None, 'side': None},
       {'variant': 'alternate', 'language': None, 'perspectives': None, 'value': 'Ogulagha II', 'between': None, 'side': None},
       {'variant': 'alternate', 'language': None, 'perspectives': None, 'value': 'Ogula', 'between': None, 'side': None}],
      dtype=object)} nan locality NG
{'primary': 'Guoghene', 'common': None, 'rules': array([{'variant': 'alternate', 'language': None, 'perspectives': None, 'value': 'Guogbene', 'between': None, 'side': None},
       {'variant': 'alternate', 'language': None, 'perspectives': None, 'value': 'Oguogbene', 'between': None, 'side': None}],
      dtype=object)} nan locality NG
{'primary': 'Sobolo-Obotobo', 'common': None, 'rules': array([{'variant': 'alternate', 'language': None, 'perspectives': None, 'value': 'Sokolobo-Obotobo', 'between': None, 'side': None}],
      dtype=object)} nan locality NG
{'primary': 'Ijaw Fishing Camp', 'common': None, 'rules': None} nan locality NG
{'primary': 'Ogoba', 'common': None, 'rules': None} nan locality NG
2:11
I can see division data. I need to understand how to get Lagos State and its LGAs. Let me look for the division_area or division_boundary subtype for admin_level, and specifically for Lagos State.

Let me look at division_area and division_boundary types, and also find the bbox for Lagos.
2:11
Bash cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow as pa

# Try division_area
reader = overturemaps.record_batch_reader('division_area', bbox=(2.0, 4.0, 15.0, 14.0))
if reader:
    table = reader.read_all()
    df = table.to_pandas()
    nigeria = df[df['country'] == 'NG']
    print('division_area columns:', list(nigeria.columns))
    print('admin_level values:', sorted(nigeria['admin_level'].dropna().unique()))
    print('subtype values:', sorted(nigeria['subtype'].dropna().unique()))
    # Find Lagos
    for _, row in nigeria.iterrows():
        name = row['names']
        if name and name.get('primary') and 'Lagos' in name['primary']:
            print('Found:', name['primary'], 'admin_level:', row['admin_level'], 'subtype:', row['subtype'])
" 2>&1
division_area columns: ['id', 'geometry', 'country', 'sources', 'subtype', 'admin_level', 'class', 'names', 'is_land', 'is_territorial', 'region', 'division_id', 'version', 'bbox']
admin_level values: [np.float64(0.0), np.float64(1.0), np.float64(2.0)]
subtype values: ['country', 'county', 'locality', 'macrohood', 'microhood', 'neighborhood', 'region']
Found: Lagos Mainland admin_level: 2.0 subtype: county
Found: Lagos admin_level: 1.0 subtype: region
Found: Lagos Island admin_level: 2.0 subtype: county
2:40
Let me find the Lagos State area and its LGAs. I need to understand the hierarchical setup. In Nigeria, "Lagos State" would be at admin_level 1 (region), and LGAs (Local Government Areas) would be at admin_level 2 (county/subtype). Let me dig deeper.
2:40
Bash cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow as pa

# Find all Lagos LGAs (county-level divisions)
reader = overturemaps.record_batch_reader('division_area', bbox=(2.0, 4.0, 15.0, 14.0))
if reader:
    table = reader.read_all()
    df = table.to_pandas()
    nigeria = df[df['country'] == 'NG']
    
    # Find the Lagos region to get its ID
    lagos_ids = []
    for _, row in nigeria.iterrows():
        name = row['names']
        if name and name.get('primary') and 'Lagos' in name['primary'] and row['subtype'] == 'region':
            print('Region:', name['primary'], 'ID:', row['id'], 'admin_level:', row['admin_level'])
            lagos_ids.append(row['id'])
    
    # Find all counties with Lagos in name
    for _, row in nigeria.iterrows():
        name = row['names']
        if name and name.get('primary') and ('Lagos' in name['primary'] or 'Ikeja' in name['primary'] or 'Eti-Osa' in name['primary']):
            print(name['primary'], 'admin_level:', row['admin_level'], 'subtype:', row['subtype'], 'id:', row['id'])
" 2>&1
Region: Lagos ID: 5fb3f6cb-3993-4bb7-8a53-73e775feb14f admin_level: 1.0
Lagos Mainland admin_level: 2.0 subtype: county id: 639f85d6-f581-4f31-b52b-98da63a57271
Ikeja admin_level: 2.0 subtype: county id: c540f662-44c9-498d-8f3d-2a41a039a29f
Lagos admin_level: 1.0 subtype: region id: 5fb3f6cb-3993-4bb7-8a53-73e775feb14f
Lagos Island admin_level: 2.0 subtype: county id: c58fd335-e502-421c-b8e3-f263c5fb0ff7
3:15
Let me understand the data better. The Overture divisions have a hierarchy. I need to get all LGA-level divisions (county subtype with admin_level 2) within the Lagos State region. Let me check if there's a `region` or `parent_division_id` field that links them.

Looking at division_area schema, there's a `division_id` field. And for division (not division_area), there's `parent_division_id` and `hierarchies`.

Let me find all counties in the Lagos region by checking all counties within the bounding box of Lagos, and then use the region/hierarchy info to filter. Or I can just use a bounding box approach - Lagos state is roughly bounded.
3:15
Bash cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow.compute as pc
import pyarrow as pa

# Read the Lagos region area geometry
reader = overturemaps.record_batch_reader('division_area', bbox=(2.0, 4.0, 15.0, 14.0))
if reader:
    table = reader.read_all()
    df = table.to_pandas()
    nigeria = df[df['country'] == 'NG']
    
    # Find Lagos region
    lagos_region = None
    for _, row in nigeria.iterrows():
        name = row['names']
        if name and name.get('primary') == 'Lagos' and row['subtype'] == 'region':
            lagos_region = row
    
    if lagos_region is not None:
        print('Lagos region id:', lagos_region['id'])
        print('Lagos region division_id:', lagos_region.get('division_id'))
        print('Lagos region geometry type:', type(lagos_region['geometry']))
        
        # Now get all counties (LGAs) with parent_division_id matching Lagos
        # Let's check the division table for parent_division_id
" 2>&1
Lagos region id: 5fb3f6cb-3993-4bb7-8a53-73e775feb14f
Lagos region division_id: 246ecf78-8622-4d7d-8d12-102e73a67cda
Lagos region geometry type: <class 'bytes'>
3:40
Let me take a different approach. I'll use the `division_boundary` type to get all LGAs in Lagos state, or I'll use DuckDB to query the Overture data with spatial filtering. Actually, the simplest approach is:

1. Get the Lagos state boundary geometry from `division_area`
2. Use that bounding box to fetch all buildings
3. Spatially filter buildings to those within Lagos state
4. Use `division_area` with subtype `county` to get LGA boundaries and assign LGAs to buildings via spatial join
5. Compute the per-LGA roll-up

Let me think about this more carefully.

The Overture divisions for Lagos State (Nigeria) - I found that "Lagos" is a region (admin_level=1), and there are subdivisions with subtype="county" (admin_level=2) like "Lagos Mainland", "Ikeja", "Lagos Island". But these are only 3 LGAs - there are actually 20 LGAs in Lagos State.

Let me look for more counties within Lagos state area.
3:40
Bash cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow as pa

# Get ALL counties (LGAs) - wider bbox for all of Nigeria
reader = overturemaps.record_batch_reader('division_area', bbox=(2.0, 4.0, 15.0, 14.0))
if reader:
    table = reader.read_all()
    df = table.to_pandas()
    nigeria = df[df['country'] == 'NG']
    
    # List all counties
    counties = nigeria[nigeria['subtype'] == 'county']
    print(f'Found {len(counties)} counties')
    for _, row in counties.iterrows():
        name = row['names']
        if name:
            print(f\"  {name.get('primary', 'N/A')}\")
    
    print()
    # Show regions
    regions = nigeria[nigeria['subtype'] == 'region']
    for _, row in regions.iterrows():
        name = row['names']
        if name:
            print(f\"Region: {name.get('primary', 'N/A')} id={row['id']}\")
" 2>&1
Found 773 counties
  Yewa North
  Imeko Afon
  Ipokia
  Badagry
  Yewa South
  Ado Odo/Ota
  Amuwo Odofin
  Ifako/Ijaye
  Ifo
  Alimosho
  Ojo
  Ibarapa North
  Abeokuta North
  Ewekoro
  Ibarapa Central
  Atisbo
  Saki West
  Iwajowa
  Baruten
  Saki East
  Shagamu
  Obafemi Owode
  Ibarapa East
  Ìsẹ́yìn
  Itesiwaju
  Kajola
  Abeokuta South
  Odeda
  Kosofe
  Shomolu
  Lagos Mainland
  Apapa
  Mushin
  Ikeja
  Agege
  Oshodi/Isolo
  Ajeromi/Ifelodun
  Surulere
  Lagos Island
  Eti Osa
  Ìkòròdú
  Epe
  Ibeju Lekki
  Oluyole
  Ibadan South West
  Ibadan North West
  Ibadan South East
  Afijio
  Ibadan North
  Ibadan North East
  Ijebu North
  Ìjẹ̀bú Òde
  Akinyele
  Atiba
  Orelope
  Oyo West
  Odogbolu
  Ido
  Remo North
  Ikenne
  Dandi
  Bunza
  Bagudo
  Irepo
  Borgu
  Suru
  Kalgo
  Arewa Dandi
  Birnin Kebbi
  Maiyama
  Kaiama
  Ijebu East
  Aiyedade
  Aiyedire
  Irewole
  Ejigbo
  Ola Oluwa
  Ogbomosho South
  Ogo Oluwa
  Ogbomosho North
  Ori Ire
  Olorunsogo
  Oyo East
  Egbeda
  Lagelu
  Ona Ara
  Ijebu North East
  Iwo
  Isokan
  Ogun Waterside
  Osogbo
  Ife East
  Ife South
  Atakunmosa West
  Ifelodun
  Oyun
  Odo Otin
  Boripe
  Offa
  Ilorin South
  Olorunda
  Moro
  Ilorin West
  Orolu
  Irepodun
  Ife Central
  Ife North
  Ede North
  Ede South
  Egbedore
  Surulere
  Asa
  Agwara
  Koko-Besse
  Jega
  Aleiro
  Argungu
  Gwandu
  Augie
  Kebbe
  Shanga
  Ngaski
  Yauri
  Tambuwal
  Silame
  Fakai
  Yabo
  Rijau
  Magama
  Ifelodun
  Ifedayo
  Isin
  Ijero
  Ekiti South-West
  Ekiti West
  Efon
  Ondo East
  Irepodun
  Ila
  Ilorin East
  Boluwaduro
  Atakunmosa East
  Ilesha East
  Obokun
  Ilesha West
  Ondo West
  Ile Oluji/Okeigbo
  Oriade
  Odigbo
  Okitipupa
  Ilaje
  Ese Odo
  Irele
  Warri North
  Ovia South-West
  Idanre
  Adó-Èkìtì
  Irepodun/Ifelodun
  Akure North
  Ekiti
  Oye
  Oke Ero
  Edu
  Ido-Osi
  Ilejemeje
  Ikere
  Akure South
  Ifedore
  Moba
  Gummi
  Shagari
  Wamako
  Sokoto North
  Bodinga
  Zuru
  Sokoto South
  Mashegu
  Mokwa
  Kontagora
  Dange-Shuni
  Tureta
  Bukkuyum
  Wasagu-Danko
  Sakaba
  Rabah
  Mariga
  Lavun
  Ose
  Akoko South West
  Ekiti East
  Ikole
  Yagba West
  Ise/Orun
  Gbonyin
  Emure
  Ọ̀wọ̀
  Ovia North-East
  Egor
  Oredo
  Warri South-West
  Burutu
  Warri South
  Ikpoba-Okha
  Sapele
  Ethiope West
  Ekeremor
  Udu
  Okpe
  Uvwie
  Uhunmwonde
  Bomadi
  Ughelli South
  Southern Ijaw
  Ethiope East
  Patani
  Ughelli North
  Orhionmwon
  Owan East
  Mopa-Muro
  Ijumu
  Akoko South East
  Owan West
  Akoko North East
  Akoko North West
  Yagba East
  Pategi
  Edati
  Wushishi
  Bakura
  Anka
  Talata Mafara
  Gbako
  Bida
  Rafi
  Maru
  Maradun
  Shinkafi
  Katcha
  Bosso
  Agaie
  Lokoja
  Adavi
  Esan North-East
  Okehi
  Etsako West
  Okene
  Kabba/Bunu
  Ogori/Magongo
  Akoko-Edo
  Esan Central
  Ika North East
  Ukwuani
  Ika South
  Iguegben
  Esan West
  Isoko South
  Isoko North
  Kolokuma/Opokuma
  Sagbama
  Ogbia
  Yenegoa
  Ndokwa West
  Nembe
  Brass
  Degema
  Akuku Toru
  Ahoada West
  Ndokwa East
  Aniocha South
  Aniocha North
  Esan South-East
  Ogba/Egbema/Ndoni
  Abua/Odual
  Ahoada East
  Emuoha
  Oguta
  Ogbaru
  Onitsha North
  Onitsha South
  Anambra West
  Oshimili South
  Oshimili North
  Idah
  Ibaji
  Lapai
  Ajaokuta
  Etsako Central
  Etsako East
  Chanchaga
  Shiroro
  Birnin Gwari
  Kaura Namoda
  Bungudu
  Zurmi
  Paikoro
  Gusau
  Birnin Magaji-Kiyaw
  Tsafe
  Sabuwa
  Faskari
  Munya
  Gurara
  Gwagwalada
  Dekina
  Ofu
  Bassa
  Igalamela-Odolu
  Kwali
  Abaji
  Kogi
  Orsu
  Nnewi South
  Dunukofia
  Nnewi North
  Anambra East
  Ekwusigo
  Idemili South
  Idemili North
  Oyi
  Oru West
  Ihiala
  Ohaji/Egbema
  Asari-Toru
  Ikwerre
  Oru East
  Owerri West
  Obio/Akpor
  Port-Harcourt
  Isu
  Mbaitoli
  Njaba
  Owerri Municipal
  Anaocha
  Njikoka
  Ayamelum
  Awka North
  Orlu
  Aguata
  Nkwerre
  Awka South
  Uzo-Uwani
  Ideato North
  Ideato South
  Ikeduru
  Ngor-Okpala
  Nwangele
  Owerri North
  Etche
  Okrika
  Eleme
  Ogu Bolo
  Ukwa West
  Omumma
  Aboh-Mbaise
  Isiala Mbano
  Orumba North
  Ezeagu
  Unuimo
  Orumba South
  Oji-River
  Udi
  Okigwe
  Ezinihitte
  Ehime Mbano
  Ahiazu-Mbaise
  Tai
  Osisioma Ngwa
  Oyigbo
  Aba North
  Aba South
  Ugwunagbo
  Obowo
  Ihitte/Uboma
  Isiala-Ngwa North
  Ukwa East
  Obi Ngwa
  Umuahia North
  Umuahia South
  Isiala-Ngwa South
  Awgu
  Isuikwuato
  Igbo-Etiti
  Umu-Nneochi
  Igbo-Eze-South
  Nsukka
  Igbo-Eze-North
  Kuje
  Toto
  Suleja
  Tafa
  Chikun
  Dandume
  Batsari
  Safana
  Dan Musa
  Funtua
  Giwa
  Kankara
  Jibia
  Kurfi
  Dutsin Ma
  Bakori
  Kaduna North
  Kaduna South
  Municipal Area Council
  Bwari
  Kagarko
  Kachia
  Igabi
  Danja
  Batagarawa
  Muhammadu Dikko Stadium
  Charanchi
  Matazu
  Malumfashi
  Zaria
  Sabon Gari
  Kudan
  Kafur
  Musawa
  Rimi
  Kankia
  Rogo
  Kajuru
  Karu
  Keffi
  Nasarawa
  Omala
  Olamaboro
  Udenu
  Ankpa
  Ogbadibo
  Isi-Uzo
  Ivo
  Aninri
  Nkanu East
  Enugu East
  Enugu South
  Nkanu West
  Enugu North
  Bende
  Ikwuano
  Ika
  Etim Ekpo
  Ukanafun
  Obot Akara
  Essien Udim
  Oruk Anam
  Ikot Ekpene
  Mkpat Enin
  Abak
  Ikono
  Ini
  Edda
  Ohaozara
  Ishielu
  Arochukwu
  Ohafia
  Etinan
  Eastern Obolo
  Ikot Abasi
  Opobo/Nkoro
  Khana
  Andoni
  Gokana
  Bonny
  Gudu
  Binji
  Tangaza
  Gwadabawa
  Illela
  Kware
  Gada
  Wurno
  Goronyo
  Sabon Birni
  Isa
  Kaita
  Onna
  Ibiono Ibom
  Nsit Ibom
  Uyo
  Onicha
  Afikpo North
  Ezza North
  Ibesikpo Asutan
  Eket
  Nsit Ubium
  Itu
  Uruan
  Nsit Atai
  Esit Eket
  Biase
  Abi
  Ezza South
  Ohaukwu
  Ado
  Ebonyi
  Odukpani
  Okobo
  Ibeno
  Urue Offong/Oruko
  Ikwo
  Yakurr
  Abakaliki
  Oron
  Mbo
  Udung Uko
  Calabar South
  Calabar Municipal
  Izzi
  Oju
  Obubra
  Boki
  Vandeikya
  Bekwarra
  Etung
  Ogoja
  Yala
  Ikom
  Akpabuyo
  Akamkpa
  Markurdi
  Nasarawa Egon
  Kaura
  Tudun Wada
  Rano
  Lere
  Bunkure
  Doguwa
  Kibiya
  Riyom
  Wamba
  Lafia
  Konshisha
  Guma
  Obi
  Keana
  Tarka
  Gboko
  Bokkos
  Barkin Ladi
  Jos North
  Sumaila
  Garko
  Jos South
  Bassa
  Warawa
  Baure
  Babura
  Gabasawa
  Wudil
  Gezawa
  Dawakin Kudu
  Minjibir
  Dambatta
  Zango
  Nassarawa
  Tarauni
  Kumbotso
  Ungongo
  Yankwashi
  Kazaure
  Makoda
  Fagge
  Kano Municipal
  Kura
  Dala
  Gwale
  Daura
  Mai'Adua
  Dutsi
  Bichi
  Kunchi
  Gwiwa
  Rimin Gado
  Tofa
  Dawakin Tofa
  Roni
  Sandamu
  Madobi
  Garum Mallam
  Bebeji
  Kubau
  Sanga
  Gwer East
  Akwanga
  Doma
  Obi
  Gwer West
  Zangon Kataf
  Jema'A
  Kauru
  Ikara
  Kiru
  Soba
  Jaba
  Oturkpo
  Kokona
  Ohimini
  Agatu
  Apa
  Okpokwu
  Makarfi
  Kusada
  Bindawa
  Mani
  Mashi
  Tsanyawa
  Gwarzo
  Shanono
  Karaye
  Kabo
  Bagwai
  Ingawa
  Machina
  Malam Maduri
  Kafin Hausa
  Auyo
  Hadejia
  Itas/Gadau
  Biriniwa
  Kanam
  Ussa
  Takum
  Bauchi
  Ganjuwa
  Shira
  Gwaram
  Jama'Are
  Ibi
  Langtang South
  Langtang North
  Wukari
  Kanke
  Warji
  Buji
  Kiyawa
  Kaugama
  Miga
  Jahun
  Maigatari
  Gagarawa
  Gumel
  Taura
  Ajingi
  Gaya
  Ringim
  Garki
  Sule Tankakar
  Dutse
  Toro
  Ningi
  Takai
  Albasu
  Jos East
  Mangu
  Qua'An Pan
  Buruku
  Ushongo
  Logo
  Awe
  Birnin Kudu
  Dass
  Tafawa-Balewa
  Bogoro
  Mikang
  Pankshin
  Shendam
  Ukum
  Katsina-Ala
  Kwande
  Obanliku
  Obudu
  Sardauna
  Kurmi
  Wase
  Donga
  Alkaleri
  Giade
  Katagum
  Kiri Kasama
  Nguru
  Zaki
  Misau
  Guri
  Gamawa
  Darazo
  Kirfi
  Gassol
  Dukku
  Karim Lamido
  Bali
  Billiri
  Shongom
  Gombe
  Akko
  Nangere
  Potiskum
  Nafada
  Barde
  Jakusko
  Damban
  Karasuwa
  Yusufari
  Borsari
  Fika
  Funakaye
  Fune
  Yamaltu/Deba
  Kaltungo
  Lau
  Kwami
  Ardo-Kola
  Gashaka
  Jalingo
  Yorro
  Balanga
  Bayo
  Larmurde
  Zing
  Teungo
  Mayo-Belwa
  Guyuk
  Kwaya Kusar
  Numan
  Gulani
  Tarmua
  Yunusari
  Gujba
  Biu
  Ngelzarma
  Geidam
  Shelleng
  Shani
  Demsa
  Ganye
  Jada
  Yola North
  Yola South
  Hawul
  Kaga
  Gubio
  Magumeri
  Damboa
  Song
  Fufore
  Gombi
  Girei
  Askira-Uba
  Hong
  Maiha
  Konduga
  Jere
  Nganzai
  Maiduguri
  Chibok
  Mobbar
  Monguno
  Mafa
  Gwoza
  Guzamala
  Abadam
  Michika
  Mubi South
  Mubi North
  Madagali
  Marte
  Kukawa
  Bama
  Dikwa
  Ngala
  Kala-Balge

Region: Oyo id=1e6cd123-0b89-4cf6-b84a-6fe0dec9d6b3
Region: Ogun id=d8e3535a-c797-4f52-a361-6f2c5116018b
Region: Lagos id=5fb3f6cb-3993-4bb7-8a53-73e775feb14f
Region: Osun id=5691ecd9-8b1c-4858-98c0-87cff32222f9
Region: Kwara id=7c895e09-4f5c-498b-8078-8d206c2ffeb6
Region: Kebbi id=e69e0ecd-90b9-4996-b993-ff4072821ace
Region: Ekiti id=769badad-946c-45f1-8761-62489c8ffd1c
Region: Ondo id=833b2001-6dde-4e77-8b40-3faadf0c9ce7
Region: Niger id=1ac1cbf8-15bc-4fb9-ae15-abc107d591de
Region: Sokoto id=ab126ab5-8213-4242-800e-cd453cdae1d8
Region: Delta id=57e7821f-04a3-4a12-baa6-80fc930bc131
Region: Edo id=d5a8660c-7516-4157-a6a2-4b3f518d9880
Region: Bayelsa id=4698eac8-abcd-4a96-b672-7162af3f6d1a
Region: Zamfara id=63dcf9f1-e17c-4391-a98e-1a5f964f0928
Region: Kogi id=77e6ff83-0ba8-432e-9a76-fa204f688ecf
Region: Rivers id=1167611b-4414-4c14-adc3-df3d9b118c93
Region: Imo id=79da8210-61a0-4aa3-89cc-053a93bc2f5a
Region: Anambra id=665cfea3-803d-4834-8175-4f21419c9bdd
Region: Enugu id=e6503528-9360-4c7e-9a18-3ff07c5e699c
Region: Federal Capital Territory id=91838175-4269-499c-afee-8a2930129d5a
Region: Kaduna id=e1a9fcd7-cce2-42bf-95da-4b18b5d20545
Region: Abia id=5224273a-e4f3-4c21-9fc9-6db1bec82325
Region: Akwa Ibom id=0a8848e8-192c-4c61-a560-0a2a9ca10563
Region: Ebonyi id=6f901e2b-f10d-4916-b609-481ad0cf66de
Region: Cross River id=155c73e5-9f36-4693-b259-d50e88a753d3
Region: Kano id=2f12c73f-b617-495f-9423-047cf513d93d
Region: Benue id=1afecc55-c117-473e-ad6b-c72a5ff19758
Region: Nasarawa id=769cca77-e3fb-4e27-aa79-ab5d0aa8ba7d
Region: Katsina id=73b76fc0-d464-4915-ae63-e02a69b8ae6d
Region: Bauchi id=4a0a1c2c-5100-483c-b613-8bd34bf0b5f8
Region: Jigawa id=a1cfd87a-38b7-492e-aaed-83c55665aed1
Region: Plateau id=9a52a2bc-bc4d-4d49-8378-0776b94d6c46
Region: Taraba id=5f6d19c5-9a0d-49e3-97f4-33158e69b688
Region: Gombe id=d277207a-67cd-43ad-8d38-25bc20066e0f
Region: Yobe id=d496707d-35eb-4f0c-985b-2658a9cc28d3
Region: Adamawa id=57b46577-6cb1-456a-8c4a-42e36b37cbc9
Region: Borno id=8567bc8d-45b2-4648-a3b5-780fbddf04c9
4:25
Now I can see the Lagos LGAs (counties). Let me get the Lagos-specific ones. From the output, the Lagos LGAs are:

1. Badagry
2. Amuwo Odofin
3. Ifako/Ijaye  (actually, this might not be Lagos, let me check)
4. Alimosho
5. Ojo
6. Kosofe
7. Shomolu
8. Lagos Mainland
9. Apapa
10. Mushin
11. Ikeja
12. Agege
13. Oshodi/Isolo
14. Ajeromi/Ifelodun
15. Surulere
16. Lagos Island
17. Eti Osa
18. Ìkòròdú (Ikorodu)
19. Epe
20. Ibeju Lekki

Let me get the ones specifically under Lagos region by checking parent_division_id or using the division hierarchy.
4:25
Bash cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow as pa

# Let's check the division table (not division_area) for parent_division_id
reader = overturemaps.record_batch_reader('division', bbox=(2.0, 4.0, 15.0, 14.0))
if reader:
    table = reader.read_all()
    df = table.to_pandas()
    nigeria = df[df['country'] == 'NG']
    
    # Find Lagos region
    lagos_region = None
    for _, row in nigeria.iterrows():
        name = row['names']
        if name and name.get('primary') == 'Lagos' and row['subtype'] == 'region':
            lagos_region = row
            break
    
    if lagos_region is not None:
        print('Lagos region id:', lagos_region['id'])
        lagos_id = lagos_region['id']
        
        # Find counties with parent_division_id = lagos_id
        lagos_counties = nigeria[(nigeria['subtype'] == 'county') & (nigeria['parent_division_id'] == lagos_id)]
        print(f'Found {len(lagos_counties)} counties under Lagos region')
        for _, row in lagos_counties.iterrows():
            name = row['names']
            if name:
                print(f\"  {name.get('primary', 'N/A')} - id: {row['id']}\")
" 2>&1
Lagos region id: 246ecf78-8622-4d7d-8d12-102e73a67cda
Found 20 counties under Lagos region
  Amuwo Odofin - id: 7583d723-71b9-4f18-9fff-bff98df8322d
  Badagry - id: 202749c9-2473-4ecb-8528-72dc7a1278ee
  Ojo - id: 2c66b8ba-4745-4334-b145-ae00b1a6f604
  Ajeromi/Ifelodun - id: b9ecb255-fc97-4e0a-8bc7-9dd0588f7a21
  Eti Osa - id: 4d40b68b-a144-47cc-9345-4604adc7fb60
  Surulere - id: 22ef6348-2be5-4543-8f1d-6e9faf24e126
  Lagos Mainland - id: 4f1aabae-5a08-4179-bdfb-f0e9d71d2ca5
  Lagos Island - id: 65a83885-eae9-4a94-8663-f6917cc9a04e
  Ibeju Lekki - id: 343b4b84-9150-4f3b-937f-c58ca20db27f
  Apapa - id: dc5547ff-cb10-4818-8e9b-363c14daa13b
  Epe - id: 053c8979-6fdf-4078-a9d1-3b545a06d72d
  Agege - id: 24e04d44-9e6e-47ba-a71c-737793381785
  Ikeja - id: af88a8c6-f84f-4629-a25b-1e79a9ccbc3d
  Ìkòròdú - id: 634b4f3c-a5c4-42d2-b18b-1432b67373b8
  Shomolu - id: 1b64c604-707e-4a65-a114-75f0f1abb3c5
  Mushin - id: fe1ca044-b547-49be-9883-2c8ffa3f04d5
  Oshodi/Isolo - id: e199f023-49ae-46ff-92a4-580c4fe1346d
  Kosofe - id: 9713c2c3-7416-41b8-9912-6e1450d3347d
  Alimosho - id: b4bd3fef-918f-4a60-bfd3-3569ed991562
  Ifako/Ijaye - id: a9df031a-eaa0-4b74-b42c-5bf303feb0e1
4:43
I found the 20 LGAs of Lagos State. Now I need to:

1. Read the building data for the Lagos bounding box
2. Filter buildings with area > 1000 m²
3. Assign each building to an LGA using spatial join
4. Compute the per-LGA summary

Let me get the bounding box for Lagos from the division_area geometry, then get buildings.

Actually, let me think about the approach more carefully:

1. Get the Lagos region geometry (from division_area) to define the bounding box
2. Read buildings within that bbox
3. Also read the division_area for the 20 LGAs (counties) in Lagos
4. Spatial join to assign LGA names to buildings
5. Filter by area > 1000 m²
6. Compute summary stats

Let me first get the bbox of Lagos.
4:43
Bash cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow as pa
from shapely import wkb
import geopandas as gpd

# Read division_area for the region geometry
reader = overturemaps.record_batch_reader('division_area', bbox=(2.0, 4.0, 15.0, 14.0))
if reader:
    table = reader.read_all()
    df = table.to_pandas()
    nigeria = df[df['country'] == 'NG']
    
    # Find Lagos region area
    lagos_area = None
    for _, row in nigeria.iterrows():
        name = row['names']
        if name and name.get('primary') == 'Lagos' and row['subtype'] == 'region':
            lagos_area = row
            break
    
    if lagos_area is not None:
        geom = wkb.loads(lagos_area['geometry'])
        bounds = geom.bounds
        print('Lagos bbox:', bounds)
        
        # Make a slightly padded bbox for building queries
        margin = 0.05
        padded = (bounds[0]-margin, bounds[1]-margin, bounds[2]+margin, bounds[3]+margin)
        print('Padded bbox:', padded)
" 2>&1
Lagos bbox: (2.7059846, 6.366684, 4.3509056, 6.6967964)
Padded bbox: (2.6559846, 6.316684, 4.4009056, 6.7467964)
5:33
Now I have all the pieces. Let me write the complete solution:

1. Get the Lagos region boundary from `division_area`
2. Get the Lagos LGA boundaries from `division` (parent_division_id = Lagos region's id) 
3. Use the division_area geometries for each LGA to clip buildings
4. Query Overture buildings for the Lagos bbox
5. Compute footprint area from geometry
6. Filter buildings > 1000 m²
7. Spatial join with LGA boundaries
8. Compute per-LGA summary stats
9. Write both output files

Let me write this as a comprehensive script.
5:33
5:33
Write /work/solve.py
content · 255 lines · py
#!/usr/bin/env python3
"""
Extract Lagos State buildings > 1000 m² from Overture Maps,
spatially join to LGAs, and produce per-LGA summary.
"""

import sys
import time

import numpy as np
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
import geopandas as gpd
from shapely import wkb
from overturemaps import record_batch_reader

# ── 1. Lagos region bbox & geometry ──────────────────────────────────────
print("Fetching Lagos region boundary ...")
for attempt in range(3):
    try:
        reader = record_batch_reader("division_area", bbox=(2.0, 4.0, 15.0, 14.0))
        break
    except Exception as exc:
        print(f"  attempt {attempt+1} failed: {exc}")
        time.sleep(2 ** attempt)
else:
    sys.exit("Could not read division_area after 3 attempts")

df_areas = reader.read_all().to_pandas()
ng_areas = df_areas[df_areas["country"] == "NG"]

lagos_region_row = None
for _, row in ng_areas.iterrows():
    name = row.get("names", {})
    if name and name.get("primary") == "Lagos" and row["subtype"] == "region":
        lagos_region_row = row
        break

if lagos_region_row is None:
    sys.exit("Could not find Lagos region in division_area")

lagos_geom = wkb.loads(lagos_region_row["geometry"])
bounds = lagos_geom.bounds
margin = 0.05
padded_bbox = (
    bounds[0] - margin,
    bounds[1] - margin,
    bounds[2] + margin,
    bounds[3] + margin,
)
print(f"  Lagos bbox: {bounds}")
print(f"  Padded bbox: {padded_bbox}")

# ── 2. Lagos LGA boundaries from division table ─────────────────────────
print("Fetching Lagos LGA boundaries ...")
for attempt in range(3):
    try:
        reader2 = record_batch_reader("division", bbox=(2.0, 4.0, 15.0, 14.0))
        break
    except Exception as exc:
        print(f"  attempt {attempt+1} failed: {exc}")
        time.sleep(2 ** attempt)
else:
    sys.exit("Could not read division after 3 attempts")

df_divisions = reader2.read_all().to_pandas()
ng_divisions = df_divisions[df_divisions["country"] == "NG"]

# Find Lagos region in division table (different ID than division_area!)
lagos_div_row = None
for _, row in ng_divisions.iterrows():
    name = row.get("names", {})
    if name and name.get("primary") == "Lagos" and row["subtype"] == "region":
        lagos_div_row = row
        break

if lagos_div_row is None:
    sys.exit("Could not find Lagos region in division table")

lagos_region_id = lagos_div_row["id"]
print(f"  Lagos region ID (division): {lagos_region_id}")

lagos_lgas = ng_divisions[
    (ng_divisions["subtype"] == "county")
    & (ng_divisions["parent_division_id"] == lagos_region_id)
].copy()

print(f"  Found {len(lagos_lgas)} LGAs:")
for _, row in lagos_lgas.iterrows():
    nm = row["names"]
    print(f"    {nm.get('primary', '?') if isinstance(nm, dict) else nm}")

# ── 3. Create a GeoDataFrame of LGA boundaries ──────────────────────────
# We need geometry from division_area, joined by id (division_id field)
print("Building LGA geometry from division_area ...")
lga_geoms = {}
for _, row in ng_areas.iterrows():
    if row["subtype"] == "county":
        lga_geoms[row["division_id"]] = wkb.loads(row["geometry"])

lga_data = []
for _, row in lagos_lgas.iterrows():
    nm = row["names"]
    lga_name = nm.get("primary", "Unknown") if isinstance(nm, dict) else str(nm)
    geom = lga_geoms.get(row["id"])
    if geom is None:
        print(f"  WARNING: no geometry for LGA {lga_name}, skipping")
        continue
    lga_data.append({"lga": lga_name, "geometry": geom})

lga_gdf = gpd.GeoDataFrame(lga_data, crs="EPSG:4326")
print(f"  {len(lga_gdf)} LGAs with geometry loaded")

# ── 4. Fetch Overture buildings in padded bbox ──────────────────────────
print("Fetching Overture buildings ...")
for attempt in range(3):
    try:
        bldg_reader = record_batch_reader("building", bbox=padded_bbox)
        break
    except Exception as exc:
        print(f"  attempt {attempt+1} failed: {exc}")
        time.sleep(2 ** attempt)
else:
    sys.exit("Could not read buildings after 3 attempts")

print("  Reading building batches ...")
batches = []
while True:
    try:
        batch = bldg_reader.read_next_batch()
    except StopIteration:
        break
    batches.append(batch)

bldg_table = pa.Table.from_batches(batches)
bldg_df = bldg_table.to_pandas()
print(f"  Total buildings fetched: {len(bldg_df)}")

# ── 5. Filter to geometry within Lagos boundary & compute area ──────────
print("Processing building geometries ...")
bldg_geoms = []
valid_mask = []
for idx, row in bldg_df.iterrows():
    try:
        geom = wkb.loads(row["geometry"])
        bldg_geoms.append(geom)
        valid_mask.append(True)
    except Exception:
        valid_mask.append(False)

bldg_df = bldg_df[valid_mask].copy()
print(f"  Valid geometries: {len(bldg_df)}")

bldg_gdf = gpd.GeoDataFrame(
    bldg_df[["id", "height"]],
    geometry=bldg_geoms,
    crs="EPSG:4326",
)

# Clip to Lagos region boundary
bldg_lagos = bldg_gdf[bldg_gdf.within(lagos_geom)].copy()
print(f"  Buildings within Lagos boundary: {len(bldg_lagos)}")

# Compute area in sq meters (project to UTM 32N — Lagos is ~3-4°E)
bldg_lagos_utm = bldg_lagos.to_crs("EPSG:32632")  # UTM zone 32N
bldg_lagos_utm["footprint_area_m2"] = bldg_lagos_utm.geometry.area

# Filter > 1000 m²
bldg_big = bldg_lagos_utm[bldg_lagos_utm["footprint_area_m2"] > 1000].copy()
print(f"  Buildings > 1000 m²: {len(bldg_big)}")

# Convert back to 4326
bldg_big = bldg_big.to_crs("EPSG:4326")

# ── 6. Spatial join to LGAs ─────────────────────────────────────────────
print("Spatially joining to LGAs ...")
bldg_with_lga = gpd.sjoin(
    bldg_big,
    lga_gdf[["lga", "geometry"]],
    how="left",
    predicate="intersects",
)

# Some buildings may intersect multiple LGAs — keep first match
bldg_with_lga = bldg_with_lga.drop_duplicates(subset="id")

# Filter to only buildings that actually matched an LGA
bldg_with_lga = bldg_with_lga[bldg_with_lga["lga"].notna()].copy()
print(f"  Buildings with LGA assignment: {len(bldg_with_lga)}")

# ── 7. Build output GeoParquet ──────────────────────────────────────────
out_cols = ["id", "height", "footprint_area_m2", "lga", "geometry"]
out_gdf = bldg_with_lga[out_cols].copy()
out_gdf["footprint_area_m2"] = out_gdf["footprint_area_m2"].round(2)

out_gdf.to_parquet(
    "/work/lagos_buildings.geoparquet",
    index=False,
    compression="zstd",
)
print(f"  Written lagos_buildings.geoparquet: {len(out_gdf)} rows")

# ── 8. Per-LGA summary ──────────────────────────────────────────────────
print("Computing per-LGA summary ...")
summary_rows = []
for lga_name, grp in bldg_with_lga.groupby("lga"):
    n_buildings = len(grp)
    total_footprint = grp["footprint_area_m2"].sum()
    n_with_height = grp["height"].notna().sum()
    if n_with_height > 0:
        p50_height = float(grp["height"].median())
    else:
        p50_height = None
    summary_rows.append(
        {
            "lga": lga_name,
            "n_buildings": n_buildings,
            "total_footprint_m2": round(total_footprint, 2),
            "n_with_height": n_with_height,
            "p50_height_m": p50_height,
        }
    )

summary_df = pd.DataFrame(summary_rows)

# Ensure all 20 LGAs appear (even if no qualifying buildings)
all_lga_names = set(
    nm.get("primary", "") if isinstance(nm, dict) else str(nm)
    for nm in lagos_lgas["names"]
)
existing_lgas = set(summary_df["lga"])
for lga_name in all_lga_names:
    if lga_name not in existing_lgas:
        summary_rows.append(
            {
                "lga": lga_name,
                "n_buildings": 0,
                "total_footprint_m2": 0.0,
                "n_with_height": 0,
                "p50_height_m": None,
            }
        )

summary_df = pd.DataFrame(summary_rows)
summary_df = summary_df.sort_values("lga").reset_index(drop=True)

summary_df.to_parquet(
    "/work/lagos_building_summary.parquet",
    index=False,
    compression="zstd",
)
print(f"  Written lagos_building_summary.parquet: {len(summary_df)} rows")

print("Done.")
Wrote /work/solve.py (8011 chars).
5:38
Wait, there's an issue with the `within` filtering of buildings against the Lagos geometry. The `lagos_geom` is from `division_area` which is a polygon geometry. But `bldg_gdf` is in EPSG:4326 and `lagos_geom` is also in 4326, so that should work. But the building geometries from Overture are multipolygons (footprints), so `.within()` should work.

However, there's a subtle issue: the division_area table gives us the region area polygon, while the division table gives us the hierarchical relationship. The `division_id` field in division_area refers to the division table's ID. Let me verify this mapping works correctly.

Also, I need to be more careful about the bbox passed to the building reader. The Overture bbox format is (west, south, east, north) which matches what I have.

Let me also consider: the buildings in the padded bbox will include some outside Lagos boundary. I'm filtering with `.within(lagos_geom)` which will keep only those fully inside. Some buildings on the boundary might be partially overlapping and could get missed - but for this use case it's acceptable.

Let me run the script and see if it works.
5:38
Bash cd /work && python solve.py 2>&1
Fetching Lagos region boundary ...
  Lagos bbox: (2.7059846, 6.366684, 4.3509056, 6.6967964)
  Padded bbox: (2.6559846, 6.316684, 4.4009056, 6.7467964)
Fetching Lagos LGA boundaries ...
  Lagos region ID (division): 246ecf78-8622-4d7d-8d12-102e73a67cda
  Found 20 LGAs:
    Amuwo Odofin
    Badagry
    Ojo
    Ajeromi/Ifelodun
    Eti Osa
    Surulere
    Lagos Mainland
    Lagos Island
    Ibeju Lekki
    Apapa
    Epe
    Agege
    Ikeja
    Ìkòròdú
    Shomolu
    Mushin
    Oshodi/Isolo
    Kosofe
    Alimosho
    Ifako/Ijaye
Building LGA geometry from division_area ...
  20 LGAs with geometry loaded
Fetching Overture buildings ...
  Reading building batches ...
  Total buildings fetched: 3687595
Processing building geometries ...
  Valid geometries: 3687595
  Buildings within Lagos boundary: 2241855
  Buildings > 1000 m²: 7375
Spatially joining to LGAs ...
  Buildings with LGA assignment: 7375
  Written lagos_buildings.geoparquet: 7375 rows
Computing per-LGA summary ...
  Written lagos_building_summary.parquet: 20 rows
Done.
12:26
Let me verify the output files to make sure they're correct.
12:26
Bash cd /work && python -c "
cd /work && python -c "
import geopandas as gpd
import pandas as pd

# Check buildings geoparquet
gdf = gpd.read_parquet('lagos_buildings.geoparquet')
print('=== lagos_buildings.geoparquet ===')
print(f'Rows: {len(gdf)}')
print(f'Columns: {list(gdf.columns)}')
print(f'CRS: {gdf.crs}')
print(gdf.head())
print()

# Check summary parquet
df = pd.read_parquet('lagos_building_summary.parquet')
print('=== lagos_building_summary.parquet ===')
print(f'Rows: {len(df)}')
print(f'Columns: {list(df.columns)}')
print(df.to_string())
"
=== lagos_buildings.geoparquet ===
Rows: 7375
Columns: ['id', 'height', 'footprint_area_m2', 'lga', 'geometry']
CRS: {"$schema": "https://proj.org/schemas/v0.7/projjson.schema.json", "type": "GeographicCRS", "name": "WGS 84", "datum_ensemble": {"name": "World Geodetic System 1984 ensemble", "members": [{"name": "World Geodetic System 1984 (Transit)"}, {"name": "World Geodetic System 1984 (G730)"}, {"name": "World Geodetic System 1984 (G873)"}, {"name": "World Geodetic System 1984 (G1150)"}, {"name": "World Geodetic System 1984 (G1674)"}, {"name": "World Geodetic System 1984 (G1762)"}, {"name": "World Geodetic System 1984 (G2139)"}, {"name": "World Geodetic System 1984 (G2296)"}], "ellipsoid": {"name": "WGS 84", "semi_major_axis": 6378137, "inverse_flattening": 298.257223563}, "accuracy": "2.0", "id": {"authority": "EPSG", "code": 6326}}, "coordinate_system": {"subtype": "ellipsoidal", "axis": [{"name": "Geodetic latitude", "abbreviation": "Lat", "direction": "north", "unit": "degree"}, {"name": "Geodetic longitude", "abbreviation": "Lon", "direction": "east", "unit": "degree"}]}, "scope": "Horizontal component of 3D system.", "area": "World.", "bbox": {"south_latitude": -90, "west_longitude": -180, "north_latitude": 90, "east_longitude": 180}, "id": {"authority": "EPSG", "code": 4326}}
                                     id  ...                                           geometry
0  e24f1bf5-e957-4867-859b-a6a578fd9b06  ...  POLYGON ((2.70836 6.37804, 2.70816 6.37804, 2....
1  5df57f70-f431-495f-98cb-eff96e1ca151  ...  POLYGON ((2.72675 6.37881, 2.72672 6.37907, 2....
2  cfb03953-1851-4ec2-a087-3283cd510753  ...  POLYGON ((2.7137 6.37984, 2.7136 6.37984, 2.71...
3  51f6d045-e5ae-4537-8eab-73c366a133bc  ...  POLYGON ((2.70833 6.38159, 2.70834 6.38147, 2....
4  a98aa38c-6427-44a7-b421-4afe08a12736  ...  POLYGON ((2.70892 6.38282, 2.70897 6.38283, 2....

[5 rows x 5 columns]

=== lagos_building_summary.parquet ===
Rows: 20
Columns: ['lga', 'n_buildings', 'total_footprint_m2', 'n_with_height', 'p50_height_m']
                 lga  n_buildings  total_footprint_m2  n_with_height  p50_height_m
0              Agege           48            95798.10              0           NaN
1   Ajeromi/Ifelodun           63           109524.81              0           NaN
2           Alimosho          359           658967.58              0           NaN
3       Amuwo Odofin          846          1778624.33              0           NaN
4              Apapa          288           616237.75              0           NaN
5            Badagry          126           212527.44              0           NaN
6                Epe          345           820969.67              0           NaN
7            Eti Osa         1088          1895665.61            241          20.0
8        Ibeju Lekki          606          1817607.97              0           NaN
9        Ifako/Ijaye          117           245337.51              0           NaN
10             Ikeja          928          3203954.37             47          15.0
11            Kosofe          167           349037.56              0           NaN
12      Lagos Island          121           232289.12              9          20.0
13    Lagos Mainland          123           231525.70              0           NaN
14            Mushin          468          1075539.67              1          35.0
15               Ojo          237           417723.04              0           NaN
16      Oshodi/Isolo          428           810638.07              3          27.0
17           Shomolu          127           205595.37              3          17.0
18          Surulere          346           791894.28              0           NaN
19           Ìkòròdú          544          1884575.21              4           7.5
12:36
Let me verify the outputs match the requirements:

1. `lagos_buildings.geoparquet` - EPSG:4326 GeoParquet with columns `id`, `height`, `footprint_area_m2`, `lga`, and `geometry` ✅
2. `lagos_building_summary.parquet` - plain Parquet with no geometry, columns `lga`, `n_buildings`, `total_footprint_m2`, `n_with_height`, and `p50_height_m` ✅

Let me also verify:
- `p50_height_m` should be null where no height data available - shows NaN which is fine in pandas/parquet
- All 20 LGAs appear ✅
- Building count vs n_with_height makes sense

Wait, I need to double-check the `p50_height_m` column. The spec says "null otherwise" (when no heights). The NaN values will be stored as null in parquet, which is correct.

Let me verify CRS is EPSG:4326 and the file structure:
12:36
Bash cd /work && python -c "
cd /work && python -c "
import pyarrow.parquet as pq
# Verify GeoParquet metadata
meta = pq.read_schema('lagos_buildings.geoparquet')
print('Buildings schema:')
print(meta)
print()

# Verify summary parquet
meta2 = pq.read_schema('lagos_building_summary.parquet')
print('Summary schema:')
print(meta2)
print()

# Check geometry column type
print('Buildings field names:', [f.name for f in meta])
print('Summary field names:', [f.name for f in meta2])
"
Buildings schema:
id: large_string
height: double
footprint_area_m2: double
lga: large_string
geometry: binary
  -- field metadata --
  ARROW:extension:name: 'geoarrow.wkb'
  ARROW:extension:metadata: '{"crs": {"$schema": "https://proj.org/schema' + 1498
-- schema metadata --
pandas: '{"index_columns": [], "column_indexes": [], "columns": [{"name":' + 657
geo: '{"primary_column": "geometry", "columns": {"geometry": {"encoding":' + 1409

Summary schema:
lga: large_string
n_buildings: int64
total_footprint_m2: double
n_with_height: int64
p50_height_m: double
-- schema metadata --
pandas: '{"index_columns": [], "column_indexes": [], "columns": [{"name":' + 699

Buildings field names: ['id', 'height', 'footprint_area_m2', 'lga', 'geometry']
Summary field names: ['lga', 'n_buildings', 'total_footprint_m2', 'n_with_height', 'p50_height_m']
12:37
Bash ls -lh /work/lagos_buildings.geoparquet /work/lagos_building_summary.parquet
-rw-r--r-- 1 agent agent 4.1K Jun 18 14:15 /work/lagos_building_summary.parquet
-rw-r--r-- 1 agent agent 1.2M Jun 18 14:15 /work/lagos_buildings.geoparquet
12:46
Let me check: the user wanted columns in `lagos_building_summary.parquet` to be `lga`, `n_buildings`, `total_footprint_m2`, `n_with_height`, `p50_height_m`. 

The summary has `p50_height_m` showing NaN for some LGAs. But the spec says "null otherwise" for median height. NaN is a float null-equivalent in Parquet, so that's fine — it will be stored as null.

Wait, I notice the `p50_height_m` column in the schema shows `double` type, and for LGAs where there are no buildings with height, it's `NaN` (which in Parquet becomes null). That's correct.

But I should check: the `p50_height_m` value for LGAs with no heights at all — I'm setting `None` in Python which becomes NaN in pandas float, which is the correct representation for null in a double column.

Let me also verify the `lga` column values. I noticed the user says "per-LGA roll-up: for each Lagos Local Government Area". So the LGAs should be the standard Lagos LGAs. Let me check them all:

1. Agege ✓
2. Ajeromi/Ifelodun ✓
3. Alimosho ✓
4. Amuwo Odofin ✓
5. Apapa ✓
6. Badagry ✓
7. Epe ✓
8. Eti Osa ✓
9. Ibeju Lekki ✓
10. Ifako/Ijaye ✓
11. Ikeja ✓
12. Kosofe ✓
13. Lagos Island ✓
14. Lagos Mainland ✓
15. Mushin ✓
16. Ojo ✓
17. Oshodi/Isolo ✓
18. Shomolu ✓
19. Surulere ✓
20. Ìkòròdú ✓

That's 20 LGAs, which matches the official 20 LGAs of Lagos State. Good.

The output files look correct. Both have the right columns, CRS for geoparquet is EPSG:4326, summary has no geometry column. Both written with zstd compression.

Let me verify the geometry column is properly stored as GeoParquet — the metadata shows `geoarrow.wkb` extension type, so it's proper GeoParquet. Good.

Everything looks good. Let me provide the final answer.
12:46