dc-l3-vienna-overpass-historical
Map (reference ← swipe → agent)
0:00
Fetch Vienna's current district polygons (Bezirke) and their 2014-01-01 counterparts from Overpass and identify which parts of the district boundaries have changed between then and now. District names changed casing and spelling between snapshots, so normalise them to match. Classify each resulting geometry fragment by how it changed: `added_since_2014` (in current but not 2014), `removed_since_2014` (in 2014 but not current), or `unchanged` (present in both snapshots). For each district flag whether its geometry borders any area that was added or removed. Output vienna_boundary_changes.geojson, MultiPolygon, each feature with change_type, district_name (normalised), and touches_changed (boolean).
0:00
System info
{
"adapter": "openrouter",
"model": "deepseek/deepseek-v4-flash",
"base_url": "https://openrouter.ai/api/v1"
}
0:07
Let me break down this task: 1. Fetch Vienna's current district polygons (Bezirke) from Overpass 2. Fetch Vienna's district polygons from 2014-01-01 3. Normalise district names (fix casing/spelling differences) 4. Compare geometries to find changes 5. Classify changes as: added_since_2014, removed_since_2014, or unchanged 6. For each district, check if it touches any changed area 7. Output as GeoJSON with MultiPolygon geometries, with change_type, district_name, touches_changed fields Let me start by writing a Python script to fetch the data from Overpass API.
0:07
Bash
ls /work/
(no output)
0:50
Let me plan this out:
1. Fetch Vienna's current district polygons (Bezirke) from Overpass API
2. Fetch Vienna's historical district polygons from 2014-01-01 (Overpass supports retrieving historical data via the "date" parameter)
3. Normalise district names (handle casing/spelling differences)
4. Compare geometries:
- `added_since_2014`: geometry present in current but not in 2014
- `removed_since_2014`: geometry present in 2014 but not in current
- `unchanged`: geometry present in both
5. For each district, flag if its geometry borders any added/removed area
6. Output as GeoJSON with MultiPolygon geometry
Let me start by querying Overpass. I'll use the Overpass API with the `[out:json]` format.
For current districts of Vienna:
```
area["name"="Wien"]->.a;
rel(area.a)[admin_level=9][boundary=administrative];
out body;
>;
out skel qt;
```
For 2014-01-01 data, Overpass supports `[date:"2014-01-01T00:00:00Z"]` as a global option.
Actually, Overpass's date handling is tricky. Let me think about how to retrieve historical data.
The Overpass API supports `[date:"YYYY-MM-DDTHH:MM:SSZ"]` to query the database as it was at that date. But this only works if the Overpass server has the historical data.
Let me try a different approach: I'll query both current and 2014 boundaries. For the historical data I can use the `[date:"2014-01-01T00:00:00Z"]` parameter.
Actually, Overpass turbo/API can handle this. Let me write a Python script that:
1. Queries Overpass for current Vienna district boundaries
2. Queries Overpass for 2014-01-01 Vienna district boundaries
3. Processes the geometries
Let me start by writing and running a Python script.
Let me think about the Overpass queries more carefully.
For getting Vienna districts (Bezirke), I need to get relations with admin_level=9 in Vienna.
Current data query:
```
[out:json];
area["name"="Wien"]->.a;
rel(area.a)[admin_level=9][boundary=administrative];
out body geom;
```
Wait, I should use `out geom;` to get geometry for each relation.
For historical data, I can try:
```
[out:json][date:"2014-01-01T00:00:00Z"];
area["name"="Wien"]->.a;
rel(area.a)[admin_level=9][boundary=administrative];
out body geom;
```
But the historical Overpass retrieval might not work perfectly. Let me try it.
Actually, let me also consider that some districts might have changed name formatting. The districts of Vienna are:
1. Innere Stadt
2. Leopoldstadt
3. Landstraße
4. Wieden
5. Margareten
6. Mariahilf
7. Neubau
8. Josefstadt
9. Alsergrund
10. Favoriten
11. Simmering
12. Meidling
13. Hietzing
14. Penzing
15. Rudolfsheim-Fünfhaus
16. Ottakring
17. Hernals
18. Währing
19. Döbling
20. Brigittenau
21. Floridsdorf
22. Donaustadt
23. Liesing
Some might be named differently (e.g., "Wien 1. Bezirk" vs "Innere Stadt", or lowercase/uppercase differences).
Let me write a comprehensive Python script.
Actually, I realize I should also consider that the Overpass API might return a lot of data. Let me start with a simple script to fetch the data.
Let me think about the approach more carefully:
1. Fetch current districts from Overpass
2. Fetch 2014 districts from Overpass
3. Normalize names (lowercase, remove common prefixes/suffixes)
4. For each district, union all their geometries (a district might have multiple way members)
5. Compare: compute symmetric difference between current and 2014 geometries for each district
6. The symmetric difference can be split into: parts only in current (added) and parts only in 2014 (removed)
7. For the "unchanged" parts, compute the intersection
8. Determine which districts touch the changed areas
Let me write this step by step.
For the Overpass query, I should use `out geom;` to get the full geometry in the response. But `out geom` on a relation gives the geometry of each way. I'll need to combine them.
Actually, for administrative boundaries, using `out geom;` on a relation returns the geometry of the ways in the relation. I need to stitch them together into a polygon.
Let me use `[out:json];` and `out geom;` for each way, then join them.
Actually a better approach: use Overpass to get simplified boundaries. Let me use `make` or `convert` to get the boundary as a single polygon.
Hmm, Overpass QL has limited geometry processing. Let me just query the relation members and process them in Python.
Let me write the script:
```python
import requests
import geopandas as gpd
from shapely.geometry import shape, MultiPolygon, Polygon
from shapely.ops import unary_union
import json
import re
from collections import defaultdict
def fetch_overpass(query, date=None):
"""Fetch data from Overpass API"""
url = "https://overpass-api.de/api/interpreter"
if date:
query = f"[date:\"{date}\"]\n{query}"
response = requests.post(url, data={"data": query})
response.raise_for_status()
return response.json()
def normalize_name(name):
"""Normalize district name for comparison"""
# Remove leading "Wien " or "Wien," or "Wien-" etc.
name = re.sub(r'^Wien[\s,.-]*', '', name, flags=re.IGNORECASE)
# Remove trailing Bezirk/Bez. etc.
name = re.sub(r'\s*Bezirk\s*$', '', name, flags=re.IGNORECASE)
name = re.sub(r'\s*Bez\.\s*$', '', name, flags=re.IGNORECASE)
# Remove leading numbers like "1.", "01.", etc.
name = re.sub(r'^\d+\.?\s*', '', name)
# Lowercase and strip
name = name.strip().lower()
return name
# Since Viennese districts are well-defined, let me build a mapping
district_names = {
'innere stadt': 'Innere Stadt',
'leopoldstadt': 'Leopoldstadt',
'landstraße': 'Landstraße',
'wieden': 'Wieden',
'margareten': 'Margareten',
'mariabilf': 'Mariahilf', # maybe typo?
'mariahilf': 'Mariahilf',
'neubau': 'Neubau',
'josefstadt': 'Josefstadt',
'alsergrund': 'Alsergrund',
'favoriten': 'Favoriten',
'simmering': 'Simmering',
'meidling': 'Meidling',
'hietzing': 'Hietzing',
'penzing': 'Penzing',
'rudolfsheim-fünfhaus': 'Rudolfsheim-Fünfhaus',
'rudolfsheim': 'Rudolfsheim-Fünfhaus',
'ottakring': 'Ottakring',
'hernals': 'Hernals',
'währing': 'Währing',
'döbling': 'Döbling',
'brigittenau': 'Brigittenau',
'floridsdorf': 'Floridsdorf',
'donaustadt': 'Donaustadt',
'liesing': 'Liesing',
}
# Overpass query for Vienna districts
query = """
[out:json];
area["name"="Wien"]->.a;
rel(area.a)[admin_level=9][boundary=administrative];
out body geom;
"""
```
Wait, let me think about this differently. The Overpass query `out geom;` on a relation gives the geometry of all member ways. But I need to convert ways to polygons. If the ways form a closed ring (which they should for a boundary), I can reconstruct the polygon.
Actually, for a boundary relation, the outer members form the boundary. Let me check...
In Overpass, for administrative boundaries, the relation has members with role "outer" (and sometimes "inner"). The geometry of the ways can be used to construct the polygon.
Let me try a simpler approach: use the Overpass `convert` to get a polygon.
Actually, let me just write the script and see what happens.
Let me first try fetching the data:
```python
import requests
import json
# Current data
query_current = """
[out:json];
area["name"="Wien"]->.a;
rel(area.a)[admin_level=9][boundary=administrative];
(._;>;);
out geom;
"""
```
Hmm, that might give a lot of data. Let me be more targeted.
Actually, let me just write the full Python script and run it. I'll fetch both datasets, process them, and output the result.
Let me think about what the output should look like:
For each district, I need to compute:
1. The "unchanged" part (intersection of current and 2014 boundaries for that district)
2. The "added_since_2014" part (current - 2014 boundary for that district)
3. The "removed_since_2014" part (2014 boundary - current for that district)
Wait, but this is about boundary changes, not area changes. So I need to compare the boundary lines, not the polygons.
Actually, let me re-read the task: "identify which parts of the district boundaries have changed" and "classify each resulting geometry fragment by how it changed".
So I think we're looking at:
- The boundary lines that are new (added)
- The boundary lines that were removed
- The boundary lines that are unchanged
And for each district, whether it "borders any area that was added or removed" (touches_changed).
Let me think about this geometrically:
1. Get the boundary lines for each district in both snapshots
2. Compute the symmetric difference of the boundary lines
3. Parts that are only in the current snapshot = added
4. Parts that are only in the 2014 snapshot = removed
5. Parts in both = unchanged
But since we're dealing with polygons (district areas), the boundary is the polygon's exterior ring.
Actually, I think the cleaner approach is:
1. Get polygon for each district for both time periods
2. For each district, compute:
- current_boundary = polygon.exterior boundary as linestring
- 2014_boundary = polygon.exterior boundary as linestring
- The difference (linestring only in current) = added
- The difference (linestring only in 2014) = removed
- The intersection = unchanged
But the boundaries are shared between adjacent districts, so we need to look at the overall boundary network.
Actually, let me reconsider. The task says "district boundaries" and "parts of the district boundaries have changed". I think the approach is:
1. Take all current district polygons
2. Take all 2014 district polygons
3. Overlay them (union/intersection/symmetric difference)
4. For each resulting fragment, determine if it's:
- In current but not 2014 → added
- In 2014 but not current → removed
- In both → unchanged
5. For each district, check if it touches any added or removed fragment
Let me go with this approach. I'll use geopandas for overlay operations.
Actually wait - I think the approach is more about the boundary lines themselves. Let me think...
The "district boundaries" are the lines that separate districts. If a boundary changes, it means the line between two districts moved. So:
1. Extract all boundary lines from current and 2014 datasets
2. Compare them
But this is complex. Let me go with the polygon overlay approach, which is cleaner:
For each district:
- Current polygon ∩ 2014 polygon = unchanged area
- Current polygon - 2014 polygon = added area (this district gained this area)
- 2014 polygon - Current polygon = removed area (this district lost this area)
Then the boundary changes are the edges of these areas.
Actually, I think a simpler interpretation: overlay all current districts with all 2014 districts. This gives us fragments. Each fragment is:
- Inside a current district (and maybe not in a 2014 district) → added
- Inside a 2014 district (and maybe not in a current district) → removed
- Inside both → unchanged
For the overlay, I can use `gpd.overlay` with `how='union'` but that's not quite right either.
Let me think about this more carefully with a simpler approach:
1. Compute the union of all current districts → this gives the area of Vienna
2. Compute the union of all 2014 districts → this gives the area of Vienna in 2014
3. Overlay these two to find areas that are:
- In current but not 2014 (Vienna expanded)
- In 2014 but not current (Vienna shrunk)
- In both (unchanged)
But that's about the city boundary, not district boundaries.
OK, let me try a different interpretation. The task is about district boundaries changing. So:
1. Take all district boundaries as linestrings
2. Compare the two sets
For the output: "MultiPolygon, each feature with change_type, district_name (normalised), and touches_changed (boolean)"
So each feature is a multipolygon. The change_type tells if it was added, removed, or unchanged.
Let me go with this approach:
1. For each district (normalized name), get the current polygon and the 2014 polygon
2. Compute:
- Intersection (unchanged)
- Current - 2014 (added_since_2014)
- 2014 - Current (removed_since_2014)
3. For each fragment, record which district it belongs to
4. Determine touches_changed: for each district, does its boundary touch any added or removed fragment?
Actually, I realize the simpler approach might be:
1. Compute the overlay of all current districts with all 2014 districts
2. This gives fragments each with a current_district_id and a 2014_district_id (can be None)
3. Classify each fragment
Let me just code this up.
Let me start by fetching the data.
Actually, let me consider that Overpass might be slow or have issues. Let me first just query to see what data we get back.
Let me write a comprehensive script.
For the Overpass query, I think `out geom;` on a relation returns the geometries of the member ways but not assembled into a polygon. I'll need to stitch them together in Python.
Actually, I just realized that `out geom;` at the relation level gives the geometry of the relation as a whole. Let me check the Overpass documentation...
Actually, in Overpass QL, `rel(...); out geom;` outputs the geometry of each way member. To get a proper polygon, I need to use `convert` or process in Python.
Let me try a different approach: Use `out geom;` on the ways (`>; out geom;`) which gives complete way geometries, then I can reconstruct polygons.
Actually, let me use `make` or the `geom` attribute in Overpass.
You know what, let me just try it and see what we get.
Let me write a script that:
1. Queries Overpass for current Vienna districts
2. Parses the response to build polygons
3. Does the same for 2014
4. Processes
Here's my plan for reconstructing polygons from Overpass relation data:
- A relation has members (ways) with roles (outer, inner)
- Each way has geometry (list of coordinates)
- I combine outer ways to form the exterior ring, inner ways for holes
But actually, Overpass might give us the ways separately. Let me use `out geom;` on the relation directly (instead of `>` then out geom on ways).
Wait, I think there's a simpler approach. Let me use the Overpass `convert` feature to output the geometry as a polygon.
Actually, I just realized there's an even simpler approach. Let me use the `geojson` output format from Overpass. Or I can use `out geom;` on the relation level.
Let me just try querying and see what the data looks like.
Let me write a quick test script.
0:50
0:50
Write
/work/fetch_test.py
content · 19 lines · py
import requests, json
# Test query for current districts
query = """
[out:json];
area["name"="Wien"]->.a;
rel(area.a)[admin_level=9][boundary=administrative];
out body geom;
"""
r = requests.post("https://overpass-api.de/api/interpreter", data={"data": query})
print(f"Status: {r.status_code}")
data = r.json()
print(f"Elements: {len(data.get('elements', []))}")
for el in data.get('elements', [])[:3]:
print(json.dumps({k:v for k,v in el.items() if k != 'members'}, indent=2)[:500])
if 'members' in el:
print(f" Members count: {len(el['members'])}")
for m in el['members'][:3]:
print(f" role={m['role']} type={m['type']} ref={m['ref']} geom={'geom' in m}")
Wrote /work/fetch_test.py (698 chars).
0:54
Let me run this test to see what the data looks like.
0:54
Bash
cd /work && python fetch_test.py
Status: 406
Traceback (most recent call last):
File "/usr/local/lib/python3.14/site-packages/requests/models.py", line 1116, in json
return complexjson.loads(self.text, **kwargs)
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.14/json/__init__.py", line 352, in loads
return _default_decoder.decode(s)
~~~~~~~~~~~~~~~~~~~~~~~^^^
File "/usr/local/lib/python3.14/json/decoder.py", line 345, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.14/json/decoder.py", line 363, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/work/fetch_test.py", line 12, in <module>
data = r.json()
File "/usr/local/lib/python3.14/site-packages/requests/models.py", line 1120, in json
raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
0:58
The Overpass API returned a 406 error. Let me try a different approach - maybe use a different URL.
0:58
Write
/work/fetch_test2.py
content · 13 lines · py
import requests, json
# Try with different content type headers
query = """
[out:json];
area["name"="Wien"]->.a;
rel(area.a)[admin_level=9][boundary=administrative];
out body geom;
"""
headers = {"Content-Type": "application/x-www-form-urlencoded"}
r = requests.post("https://overpass-api.de/api/interpreter", data={"data": query}, headers=headers)
print(f"Status: {r.status_code}")
print(f"Response preview: {r.text[:500]}")
Wrote /work/fetch_test2.py (426 chars).
1:01
The issue might be that Overpass API expects a different content type or the data format. Let me try a different approach.
1:01
Bash
cd /work && python fetch_test2.py
Status: 406 Response preview: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html><head> <title>406 Not Acceptable</title> </head><body> <h1>Not Acceptable</h1> <p>An appropriate representation of the requested resource could not be found on this server.</p> <hr> <address>Apache/2.4.67 (Debian) Server at overpass-api.de Port 443</address> </body></html>
1:07
Let me try a different approach - maybe I need to use a different Overpass endpoint, or use the proper content type. Let me try with curl or specifying Accept header.
1:07
Write
/work/fetch_test3.py
content · 15 lines · py
import requests, json
query = """
[out:json];
area["name"="Wien"]->.a;
rel(area.a)[admin_level=9][boundary=administrative];
out body geom;
"""
headers = {"Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json"}
r = requests.post("https://overpass-api.de/api/interpreter", data={"data": query}, headers=headers)
print(f"Status: {r.status_code}")
print(f"Response preview: {r.text[:500]}")
if r.status_code == 200:
data = r.json()
print(f"Elements: {len(data.get('elements', []))}")
Wrote /work/fetch_test3.py (515 chars).
1:11
Bash
cd /work && python fetch_test3.py
Status: 406 Response preview: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html><head> <title>406 Not Acceptable</title> </head><body> <h1>Not Acceptable</h1> <p>An appropriate representation of the requested resource could not be found on this server.</p> <hr> <address>Apache/2.4.67 (Debian) Server at overpass-api.de Port 443</address> </body></html>
1:18
The overpass-api.de seems to not like something about the request. Let me try using the overpass turbo URL or another endpoint.
1:18
Write
/work/fetch_test4.py
content · 27 lines · py
import requests, json
query = """
[out:json];
area["name"="Wien"]->.a;
rel(area.a)[admin_level=9][boundary=administrative];
out body geom;
"""
# Try alternative overpass server
urls = [
"https://overpass-api.de/api/interpreter",
"https://lz4.overpass-api.de/api/interpreter",
"https://overpass.kumi.systems/api/interpreter",
]
for url in urls:
try:
r = requests.post(url, data={"data": query}, timeout=30)
print(f"URL: {url}")
print(f"Status: {r.status_code}")
if r.status_code == 200:
data = r.json()
print(f"Elements: {len(data.get('elements', []))}")
break
else:
print(f"Response: {r.text[:200]}")
except Exception as e:
print(f"Error with {url}: {e}")
Wrote /work/fetch_test4.py (772 chars).
1:21
响应
1:21
Bash
cd /work && python fetch_test4.py
URL: https://overpass-api.de/api/interpreter Status: 406 Response: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html><head> <title>406 Not Acceptable</title> </head><body> <h1>Not Acceptable</h1> <p>An appropriate repres URL: https://lz4.overpass-api.de/api/interpreter Status: 406 Response: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html><head> <title>406 Not Acceptable</title> </head><body> <h1>Not Acceptable</h1> <p>An appropriate repres Error with https://overpass.kumi.systems/api/interpreter: HTTPSConnectionPool(host='overpass.kumi.systems', port=443): Read timed out. (read timeout=30)
1:59
The 406 error suggests there's a content negotiation issue. Let me try with curl instead and see what's happening.
1:59
Bash
curl -s -o /tmp/response.txt -w "%{http_code}" -X POST -d 'data=[out:json];area[...
bash: line 1: curl: command not found
2:02
Let me install curl or use a different approach.
2:02
Bash
pip install --user curl && python -c "import urllib.request, json
pip install --user curl && python -c "import urllib.request, json
query = '[out:json];area[\"name\"=\"Wien\"]->.a;rel(area.a)[admin_level=9][boundary=administrative];out body geom;'
print(query)
data = urllib.parse.urlencode({'data': query}).encode()
req = urllib.request.Request('https://overpass-api.de/api/interpreter', data=data, headers={'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded'})
resp = urllib.request.urlopen(req, timeout=60)
print(resp.status)
text = resp.read().decode()
print(text[:300])
"
ERROR: Could not find a version that satisfies the requirement curl (from versions: none) ERROR: No matching distribution found for curl
2:10
Let me check the urllib approach properly.
2:10
Write
/work/fetch_test5.py
content · 13 lines · py
import urllib.request, urllib.parse, json
query = '[out:json];area["name"="Wien"]->.a;rel(area.a)[admin_level=9][boundary=administrative];out body geom;'
data = urllib.parse.urlencode({'data': query}).encode()
req = urllib.request.Request('https://overpass-api.de/api/interpreter', data=data, headers={'Accept': 'application/json'})
resp = urllib.request.urlopen(req, timeout=120)
print(f"Status: {resp.status}")
text = resp.read().decode()
print(f"Length: {len(text)}")
print(text[:500])
if resp.status == 200:
obj = json.loads(text)
print(f"Elements: {len(obj.get('elements',[]))}")
Wrote /work/fetch_test5.py (593 chars).
2:14
Bash
cd /work && python fetch_test5.py
Traceback (most recent call last):
File "/work/fetch_test5.py", line 6, in <module>
resp = urllib.request.urlopen(req, timeout=120)
File "/usr/local/lib/python3.14/urllib/request.py", line 187, in urlopen
return opener.open(url, data, timeout)
~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.14/urllib/request.py", line 493, in open
response = meth(req, response)
File "/usr/local/lib/python3.14/urllib/request.py", line 602, in http_response
response = self.parent.error(
'http', request, response, code, msg, hdrs)
File "/usr/local/lib/python3.14/urllib/request.py", line 531, in error
return self._call_chain(*args)
~~~~~~~~~~~~~~~~^^^^^^^
File "/usr/local/lib/python3.14/urllib/request.py", line 464, in _call_chain
result = func(*args)
File "/usr/local/lib/python3.14/urllib/request.py", line 611, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 406: Not Acceptable
2:19
The Overpass API is returning 406 because of the Accept header. Let me try without the Accept header, or use a different approach.
2:19
Write
/work/fetch_test6.py
content · 13 lines · py
import urllib.request, urllib.parse, json
query = '[out:json];area["name"="Wien"]->.a;rel(area.a)[admin_level=9][boundary=administrative];out body geom;'
data = urllib.parse.urlencode({'data': query}).encode()
req = urllib.request.Request('https://overpass-api.de/api/interpreter', data=data)
resp = urllib.request.urlopen(req, timeout=120)
print(f"Status: {resp.status}")
text = resp.read().decode()
print(f"Length: {len(text)}")
print(text[:500])
if resp.status == 200:
obj = json.loads(text)
print(f"Elements: {len(obj.get('elements',[]))}")
Wrote /work/fetch_test6.py (553 chars).
2:23
Bash
cd /work && python fetch_test6.py
Traceback (most recent call last):
File "/work/fetch_test6.py", line 6, in <module>
resp = urllib.request.urlopen(req, timeout=120)
File "/usr/local/lib/python3.14/urllib/request.py", line 187, in urlopen
return opener.open(url, data, timeout)
~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.14/urllib/request.py", line 493, in open
response = meth(req, response)
File "/usr/local/lib/python3.14/urllib/request.py", line 602, in http_response
response = self.parent.error(
'http', request, response, code, msg, hdrs)
File "/usr/local/lib/python3.14/urllib/request.py", line 531, in error
return self._call_chain(*args)
~~~~~~~~~~~~~~~~^^^^^^^
File "/usr/local/lib/python3.14/urllib/request.py", line 464, in _call_chain
result = func(*args)
File "/usr/local/lib/python3.14/urllib/request.py", line 611, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 406: Not Acceptable
2:33
The 406 error is coming from the Overpass server. Let me check if there's a custom User-Agent or something else needed. Let me try using requests with a proper User-Agent and Accept header.
2:33
Write
/work/fetch_test7.py
content · 12 lines · py
import requests, json
query = '[out:json];area["name"="Wien"]->.a;rel(area.a)[admin_level=9][boundary=administrative];out body geom;'
headers = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) Python/3.14",
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
}
r = requests.post("https://overpass-api.de/api/interpreter", data={"data": query}, headers=headers, timeout=120)
print(f"Status: {r.status_code}")
print(f"Headers: {dict(r.headers)}")
print(f"Response: {r.text[:500]}")
Wrote /work/fetch_test7.py (555 chars).
2:36
The 406 error is about content negotiation. Let me try with a different approach - maybe the overpass server has changed its requirements. Actually, looking at the error it's a 406 Not Acceptable which typically means the server can't produce a response matching the Accept headers sent by the client. Let me try without any specific headers.
2:36
Bash
cd /work && python fetch_test7.py
Status: 406
Headers: {'Date': 'Thu, 18 Jun 2026 23:09:43 GMT', 'Server': 'Apache/2.4.67 (Debian)', 'Content-Length': '371', 'Connection': 'close', 'Content-Type': 'text/html; charset=iso-8859-1'}
Response: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><head>
<title>406 Not Acceptable</title>
</head><body>
<h1>Not Acceptable</h1>
<p>An appropriate representation of the requested resource could not be found on this server.</p>
<hr>
<address>Apache/2.4.67 (Debian) Server at overpass-api.de Port 443</address>
</body></html>
2:42
The 406 error seems odd. Let me check if there's an issue with the query syntax. Perhaps the double quotes inside the query string are causing issues. Let me try a simpler query.
2:42
Write
/work/fetch_test8.py
content · 25 lines · py
import requests, json
# Simplified query - no area filter first
query = """
[out:json];
rel(3860455);
out body geom;
"""
# Try using osm3s to prepend _[out:json] differently
query2 = "[out:json];rel(3860455);out body geom;"
# Also try getting node
query3 = "[out:json];node(1);out;"
for i, q in enumerate([query, query2, query3], 1):
try:
r = requests.post("https://overpass-api.de/api/interpreter", data={"data": q.strip()}, timeout=30)
print(f"Query {i}: Status={r.status_code}")
if r.status_code == 200:
print(f" Elements: {len(r.json().get('elements',[]))}")
print(f" Text: {r.text[:300]}")
else:
print(f" Error: {r.text[:200]}")
except Exception as e:
print(f"Query {i}: Exception: {e}")
Wrote /work/fetch_test8.py (782 chars).
2:47
Bash
cd /work && python fetch_test8.py
Query 1: Status=406 Error: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html><head> <title>406 Not Acceptable</title> </head><body> <h1>Not Acceptable</h1> <p>An appropriate repres Query 2: Status=406 Error: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html><head> <title>406 Not Acceptable</title> </head><body> <h1>Not Acceptable</h1> <p>An appropriate repres Query 3: Status=406 Error: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html><head> <title>406 Not Acceptable</title> </head><body> <h1>Not Acceptable</h1> <p>An appropriate repres
2:52
The Overpass API is returning 406 for all queries. This might be because the server is not accepting our requests. Let me try the `wget` command or see if we have `wget` or `httpie` available.
2:52
Bash
which wget httpie alpine curl 2>/dev/null; apt list --installed 2>/dev/null | gr...
libcurl3t64-gnutls/now 8.14.1-2+deb13u3 amd64 [installed,local] libcurl4-openssl-dev/now 8.14.1-2+deb13u3 amd64 [installed,local] libcurl4t64/now 8.14.1-2+deb13u3 amd64 [installed,local]
2:56
The Overpass API is returning 406 Not Acceptable. This is odd. Let me check if the API endpoint has changed or if there's an issue with the query format. Maybe the server requires a specific query format or there's a rate limiting issue. Let me try using a different URL or approach. Actually, let me try using the `curl` command line tool more carefully. Actually curl is not installed. Let me install it.
2:56
Bash
apt-get install -y curl 2>&1 | tail -5
E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied) E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), are you root?
3:00
Let me try installing curl via pip. Actually let me look for it via which or find.
3:00
Bash
find / -name curl -type f 2>/dev/null; pip install --user pycurl 2>&1 | tail -3
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 5.1/5.1 MB 12.2 MB/s 0:00:00 Installing collected packages: pycurl Successfully installed pycurl-7.46.0
3:07
Let me try using pycurl or the requests library more carefully. The 406 error might be related to the Accept header. Let me try without any Accept header, or with different approaches.
3:07
Write
/work/fetch_test9.py
content · 12 lines · py
import json, io, urllib.request, urllib.parse
query = '[out:json];area["name"="Wien"]->.a;rel(area.a)[admin_level=9][boundary=administrative];out body geom;'
data = urllib.parse.urlencode({'data': query}).encode()
req = urllib.request.Request('https://overpass-api.de/api/interpreter', data=data)
# Remove all accept headers by not setting one
resp = urllib.request.urlopen(req, timeout=120)
print(f"Status: {resp.status}")
print(f"Headers: {dict(resp.headers)}")
text = resp.read().decode()
print(f"Length: {len(text)}")
print(text[:500])
Wrote /work/fetch_test9.py (540 chars).
3:10
3:10
Bash
cd /work && python fetch_test9.py
Traceback (most recent call last):
File "/work/fetch_test9.py", line 7, in <module>
resp = urllib.request.urlopen(req, timeout=120)
File "/usr/local/lib/python3.14/urllib/request.py", line 187, in urlopen
return opener.open(url, data, timeout)
~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.14/urllib/request.py", line 493, in open
response = meth(req, response)
File "/usr/local/lib/python3.14/urllib/request.py", line 602, in http_response
response = self.parent.error(
'http', request, response, code, msg, hdrs)
File "/usr/local/lib/python3.14/urllib/request.py", line 531, in error
return self._call_chain(*args)
~~~~~~~~~~~~~~~~^^^^^^^
File "/usr/local/lib/python3.14/urllib/request.py", line 464, in _call_chain
result = func(*args)
File "/usr/local/lib/python3.14/urllib/request.py", line 611, in http_error_default
raise HTTPError(req.full_url, code, msg, hdrs, fp)
urllib.error.HTTPError: HTTP Error 406: Not Acceptable
3:21
The issue might be that the Overpass API requires a specific User-Agent. Let me try pycurl which I just installed.
3:21
Write
/work/fetch_test10.py
content · 19 lines · py
import pycurl, io, json, urllib.parse
query = '[out:json];area["name"="Wien"]->.a;rel(area.a)[admin_level=9][boundary=administrative];out body geom;'
buf = io.BytesIO()
c = pycurl.Curl()
c.setopt(c.URL, 'https://overpass-api.de/api/interpreter')
c.setopt(c.POSTFIELDS, 'data=' + urllib.parse.quote(query))
c.setopt(c.WRITEDATA, buf)
c.setopt(c.TIMEOUT, 120)
c.perform()
status = c.getinfo(c.RESPONSE_CODE)
body = buf.getvalue().decode('utf-8')
print(f"Status: {status}")
print(f"Length: {len(body)}")
print(body[:500])
if status == 200:
obj = json.loads(body)
print(f"Elements: {len(obj.get('elements',[]))}")
Wrote /work/fetch_test10.py (619 chars).
3:38
Bash
cd /work && python fetch_test10.py
Status: 200
Length: 665643
{
"version": 0.6,
"generator": "Overpass API 0.7.62.11 87bfad18",
"osm3s": {
"timestamp_osm_base": "2026-06-18T23:08:53Z",
"timestamp_areas_base": "2026-06-18T08:44:00Z",
"copyright": "The data included in this document is from www.openstreetmap.org. The data is made available under ODbL."
},
"elements": [
{
"type": "relation",
"id": 1990590,
"bounds": {
"minlat": 48.2138140,
"minlon": 16.3408703,
"maxlat": 48.2363090,
"maxlon": 16.3701490
},
"mem
Elements: 23
3:51
Great, I got the data! Now I need to also get the 2014-01-01 snapshot. I can use Overpass with the date parameter. Let me also check each relation's details.
3:51
3:51
Write
/work/fetch_current.py
content · 25 lines · py
import pycurl, io, json, urllib.parse, pickle
query = '[out:json];area["name"="Wien"]->.a;rel(area.a)[admin_level=9][boundary=administrative];out body geom;'
buf = io.BytesIO()
c = pycurl.Curl()
c.setopt(c.URL, 'https://overpass-api.de/api/interpreter')
c.setopt(c.POSTFIELDS, 'data=' + urllib.parse.quote(query))
c.setopt(c.WRITEDATA, buf)
c.setopt(c.TIMEOUT, 120)
c.perform()
status = c.getinfo(c.RESPONSE_CODE)
body = buf.getvalue().decode('utf-8')
print(f"Status: {status}, Length: {len(body)}")
obj = json.loads(body)
print(f"Elements: {len(obj.get('elements',[]))}")
with open('/work/current_districts.json', 'w') as f:
json.dump(obj, f)
print("Saved current_districts.json")
# Show element types and names
for el in obj['elements']:
tags = el.get('tags', {})
name = tags.get('name', '?')
print(f" id={el['id']} type={el['type']} name={name}")
Wrote /work/fetch_current.py (869 chars).
3:53
Bash
cd /work && python fetch_current.py
Status: 200, Length: 665643 Elements: 23 Saved current_districts.json id=1990590 type=relation name=Alsergrund id=1990591 type=relation name=Hietzing id=1990592 type=relation name=Innere Stadt id=1990593 type=relation name=Josefstadt id=1990594 type=relation name=Leopoldstadt id=1990595 type=relation name=Mariahilf id=1990596 type=relation name=Meidling id=1990597 type=relation name=Neubau id=1990598 type=relation name=Penzing id=1990599 type=relation name=Rudolfsheim-Fünfhaus id=1990600 type=relation name=Währing id=1991416 type=relation name=Landstraße id=1991433 type=relation name=Brigittenau id=1991434 type=relation name=Donaustadt id=1991435 type=relation name=Döbling id=1991436 type=relation name=Favoriten id=1991437 type=relation name=Floridsdorf id=1991438 type=relation name=Hernals id=1991439 type=relation name=Liesing id=1991440 type=relation name=Margareten id=1991441 type=relation name=Ottakring id=1991442 type=relation name=Simmering id=1991443 type=relation name=Wieden
4:01
Now I need to get the 2014-01-01 data. For that, I can use Overpass's date suffix on `out` or use the "date" parameter in the query. Let me try using the `[date:"2014-01-01T00:00:00Z"]` syntax.
4:01
4:01
Write
/work/fetch_2014.py
content · 27 lines · py
import pycurl, io, json, urllib.parse
# Use the 'date' parameter in the [out:json] or use a query with date
query = '[out:json][date:"2014-01-01T00:00:00Z"];area["name"="Wien"]->.a;rel(area.a)[admin_level=9][boundary=administrative];out body geom;'
buf = io.BytesIO()
c = pycurl.Curl()
c.setopt(c.URL, 'https://overpass-api.de/api/interpreter')
c.setopt(c.POSTFIELDS, 'data=' + urllib.parse.quote(query))
c.setopt(c.WRITEDATA, buf)
c.setopt(c.TIMEOUT, 120)
c.perform()
status = c.getinfo(c.RESPONSE_CODE)
body = buf.getvalue().decode('utf-8')
print(f"Status: {status}, Length: {len(body)}")
if status == 200:
obj = json.loads(body)
print(f"Elements: {len(obj.get('elements',[]))}")
with open('/work/districts_2014.json', 'w') as f:
json.dump(obj, f)
print("Saved districts_2014.json")
for el in obj['elements']:
tags = el.get('tags', {})
name = tags.get('name', '?')
print(f" id={el['id']} type={el['type']} name={name}")
else:
print(body[:500])
Wrote /work/fetch_2014.py (1004 chars).
4:02
Bash
cd /work && python fetch_2014.py
<exit_code>-1</exit_code> <error>Command timed out after 120000ms</error>
6:09
The 2014 snapshot query timed out. Let me try a different approach - maybe use the Overpass API with a different endpoint or try the attic queries differently.
6:09
Write
/work/fetch_2014b.py
content · 33 lines · py
import pycurl, io, json, urllib.parse
# Method: use the 'out' modifier with 'geom' and specify date in the request
# Actually Overpass supports [date:"YYYY-MM-DDTHH:MM:SSZ"] in the query header
# But maybe the server doesn't have attic data for that date.
# Let's try without the area filter first, get all relations with admin_level=9 in Vienna
# Try a different syntax - put date right at the beginning
query = '[date:"2014-01-01T00:00:00Z"][out:json];area["name"="Wien"]->.a;rel(area.a)[admin_level=9][boundary=administrative];out body geom;'
buf = io.BytesIO()
c = pycurl.Curl()
c.setopt(c.URL, 'https://overpass-api.de/api/interpreter')
c.setopt(c.POSTFIELDS, 'data=' + urllib.parse.quote(query))
c.setopt(c.WRITEDATA, buf)
c.setopt(c.TIMEOUT, 120)
c.setopt(c.NOSIGNAL, 1)
c.perform()
status = c.getinfo(c.RESPONSE_CODE)
body = buf.getvalue().decode('utf-8')
print(f"Status: {status}, Length: {len(body)}")
if status == 200:
obj = json.loads(body)
print(f"Elements: {len(obj.get('elements',[]))}")
with open('/work/districts_2014.json', 'w') as f:
json.dump(obj, f)
print("Saved districts_2014.json")
for el in obj['elements']:
tags = el.get('tags', {})
name = tags.get('name', '?')
print(f" id={el['id']} type={el['type']} name={name}")
else:
print(body[:500])
Wrote /work/fetch_2014b.py (1327 chars).
6:15
The timeout might be because area data doesn't exist for 2014. Let me try a different approach - get the data without using area, using the known relation IDs, or use the attic queries differently.
6:15
Write
/work/fetch_2014c.py
content · 32 lines · py
import pycurl, io, json, urllib.parse
# Let's get all Vienna districts by their relation IDs directly
# First get current to find the IDs
# Actually we know they're 1990590-1990600, 1991416, 1991433-1991443
# Let's try with a simpler query first to see if attic data works
# Try getting a single relation at that date
query = '[out:json][date:"2014-01-01T00:00:00Z"];rel(1990590);out body geom;'
buf = io.BytesIO()
c = pycurl.Curl()
c.setopt(c.URL, 'https://overpass-api.de/api/interpreter')
c.setopt(c.POSTFIELDS, 'data=' + urllib.parse.quote(query))
c.setopt(c.WRITEDATA, buf)
c.setopt(c.TIMEOUT, 120)
c.setopt(c.NOSIGNAL, 1)
c.perform()
status = c.getinfo(c.RESPONSE_CODE)
body = buf.getvalue().decode('utf-8')
print(f"Status: {status}, Length: {len(body)}")
if status == 200:
obj = json.loads(body)
print(f"Elements: {len(obj.get('elements',[]))}")
for el in obj['elements']:
tags = el.get('tags', {})
name = tags.get('name', '?')
print(f" id={el['id']} name={name}")
with open('/work/test_2014.json', 'w') as f:
json.dump(obj, f)
else:
print(body[:500])
Wrote /work/fetch_2014c.py (1114 chars).
6:17
Let me try a different approach - first understand if the attic data is available and how it works.
6:17
Bash
cd /work && timeout 60 python fetch_2014c.py
Status: 200, Length: 11591 Elements: 1 id=1990590 name=Alsergrund
6:23
So attic data works for individual relations. Let me get all 23 relations from 2014-01-01.
6:23
6:23
Write
/work/fetch_2014_all.py
content · 35 lines · py
import pycurl, io, json, urllib.parse
# Get all 23 Vienna district relations from 2014-01-01
ids = [1990590, 1990591, 1990592, 1990593, 1990594, 1990595, 1990596,
1990597, 1990598, 1990599, 1990600, 1991416, 1991433, 1991434,
1991435, 1991436, 1991437, 1991438, 1991439, 1991440, 1991441,
1991442, 1991443]
# Build query for all relations
rel_ids = ','.join(str(i) for i in ids)
query = f'[out:json][date:"2014-01-01T00:00:00Z"];rel({rel_ids});out body geom;'
buf = io.BytesIO()
c = pycurl.Curl()
c.setopt(c.URL, 'https://overpass-api.de/api/interpreter')
c.setopt(c.POSTFIELDS, 'data=' + urllib.parse.quote(query))
c.setopt(c.WRITEDATA, buf)
c.setopt(c.TIMEOUT, 300)
c.setopt(c.NOSIGNAL, 1)
c.perform()
status = c.getinfo(c.RESPONSE_CODE)
body = buf.getvalue().decode('utf-8')
print(f"Status: {status}, Length: {len(body)}")
if status == 200:
obj = json.loads(body)
print(f"Elements: {len(obj.get('elements',[]))}")
with open('/work/districts_2014.json', 'w') as f:
json.dump(obj, f)
print("Saved districts_2014.json")
for el in obj['elements']:
tags = el.get('tags', {})
name = tags.get('name', '?')
print(f" id={el['id']} name={name}")
else:
print(body[:1000])
Wrote /work/fetch_2014_all.py (1247 chars).
6:25
Bash
cd /work && timeout 120 python fetch_2014_all.py
Status: 400, Length: 1105
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" lang="en"/>
<title>OSM3S Response</title>
</head>
<body>
<p>The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.</p>
<p><strong style="color:#FF0000">Error</strong>: line 1: static error: For the attribute "n" of the element "bbox-query" the only allowed values are floats between -90.0 and 90.0. </p>
<p><strong style="color:#FF0000">Error</strong>: line 1: static error: For the attribute "w" of the element "bbox-query" the only allowed values are floats between -180.0 and 180.0. </p>
<p><strong style="color:#FF0000">Error</strong>: line 1: static error: For the attribute "e" of the element &
6:31
The query seems to be too long or has some issue. Let me try fetching them one by one or in smaller batches.
6:31
Write
/work/fetch_2014_batch.py
content · 36 lines · py
import pycurl, io, json, urllib.parse, time
ids = [1990590, 1990591, 1990592, 1990593, 1990594, 1990595, 1990596,
1990597, 1990598, 1990599, 1990600, 1991416, 1991433, 1991434,
1991435, 1991436, 1991437, 1991438, 1991439, 1991440, 1991441,
1991442, 1991443]
all_elements = []
for rid in ids:
query = f'[out:json][date:"2014-01-01T00:00:00Z"];rel({rid});out body geom;'
buf = io.BytesIO()
c = pycurl.Curl()
c.setopt(c.URL, 'https://overpass-api.de/api/interpreter')
c.setopt(c.POSTFIELDS, 'data=' + urllib.parse.quote(query))
c.setopt(c.WRITEDATA, buf)
c.setopt(c.TIMEOUT, 120)
c.setopt(c.NOSIGNAL, 1)
c.perform()
status = c.getinfo(c.RESPONSE_CODE)
body = buf.getvalue().decode('utf-8')
if status == 200:
obj = json.loads(body)
for el in obj['elements']:
all_elements.append(el)
tags = el.get('tags', {})
name = tags.get('name', '?')
print(f" id={el['id']} name={name}")
else:
print(f" id={rid} status={status} error={body[:200]}")
time.sleep(2) # be nice to the server
print(f"\nTotal elements: {len(all_elements)}")
with open('/work/districts_2014.json', 'w') as f:
json.dump({"version": 0.6, "generator": "Overpass", "elements": all_elements}, f)
print("Saved districts_2014.json")
Wrote /work/fetch_2014_batch.py (1347 chars).
6:34
Bash
cd /work && timeout 300 python fetch_2014_batch.py
id=1990590 name=Alsergrund
id=1990591 name=Hietzing
id=1990592 name=Innere Stadt
id=1990593 name=Josefstadt
id=1990594 name=Leopoldstadt
id=1990595 name=Mariahilf
id=1990596 name=Meidling
id=1990597 name=Neubau
id=1990598 name=Penzing
id=1990599 name=Rudolfsheim-Fünfhaus
id=1990600 name=Währing
id=1991416 name=Landstraße
id=1991433 name=Brigittenau
id=1991434 name=Donaustadt
id=1991435 name=Döbling
id=1991436 name=Favoriten
id=1991437 name=Floridsdorf
id=1991438 name=Hernals
id=1991439 name=Liesing
id=1991440 name=Margareten
id=1991441 name=Ottakring
id=1991442 status=429 error=<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:
id=1991443 status=429 error=<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:
Total elements: 21
Saved districts_2014.json
8:48
Got rate limited. Let me retry the remaining two (1991442 Simmering, 1991443 Wieden) after a wait.
8:48
Write
/work/fetch_remaining.py
content · 36 lines · py
import pycurl, io, json, urllib.parse, time
ids = [1991442, 1991443]
all_elements = []
for rid in ids:
query = f'[out:json][date:"2014-01-01T00:00:00Z"];rel({rid});out body geom;'
buf = io.BytesIO()
c = pycurl.Curl()
c.setopt(c.URL, 'https://overpass-api.de/api/interpreter')
c.setopt(c.POSTFIELDS, 'data=' + urllib.parse.quote(query))
c.setopt(c.WRITEDATA, buf)
c.setopt(c.TIMEOUT, 120)
c.setopt(c.NOSIGNAL, 1)
c.setopt(c.FRESH_CONNECT, 1)
c.perform()
status = c.getinfo(c.RESPONSE_CODE)
body = buf.getvalue().decode('utf-8')
if status == 200:
obj = json.loads(body)
for el in obj['elements']:
all_elements.append(el)
tags = el.get('tags', {})
name = tags.get('name', '?')
print(f" id={el['id']} name={name}")
else:
print(f" id={rid} status={status}")
print(f" {body[:200]}")
time.sleep(5)
with open('/work/districts_2014.json', 'r') as f:
data = json.load(f)
data['elements'].extend(all_elements)
with open('/work/districts_2014.json', 'w') as f:
json.dump(data, f)
print(f"Total: {len(data['elements'])}")
Wrote /work/fetch_remaining.py (1162 chars).
8:49
Bash
cd /work && timeout 120 python fetch_remaining.py
id=1991442 name=Simmering id=1991443 name=Wieden Total: 23
9:07
Got all 23 districts from both current and 2014. Now let me examine the data structures more closely, particularly the relation members and geometry.
9:07
9:07
Write
/work/examine.py
content · 41 lines · py
import json
with open('/work/current_districts.json') as f:
cur = json.load(f)
with open('/work/districts_2014.json') as f:
old = json.load(f)
# Examine a relation structure
for el in cur['elements']:
if el['id'] == 1990592: # Innere Stadt
print("=== Current Innere Stadt ===")
print(f"Tags: {json.dumps(el.get('tags',{}), indent=2)}")
mems = el.get('members', [])
print(f"Members: {len(mems)}")
for m in mems[:5]:
print(f" role={m['role']} type={m['type']} ref={m['ref']} geom={'geom' in m}")
# Check if any member has full geometry
for m in mems:
if 'geom' in m:
print(f" Member with geom: role={m['role']} type={m['type']}")
print(f" Geometry points: {len(m['geom'])}")
print(f" First 3: {m['geom'][:3]}")
break
break
# Same for 2014
for el in old['elements']:
if el['id'] == 1990592:
print("\n=== 2014 Innere Stadt ===")
print(f"Tags: {json.dumps(el.get('tags',{}), indent=2)}")
mems = el.get('members', [])
print(f"Members: {len(mems)}")
for m in mems[:5]:
print(f" role={m['role']} type={m['type']} ref={m['ref']} geom={'geom' in m}")
for m in mems:
if 'geom' in m:
print(f" Member with geom: role={m['role']} type={m['type']}")
print(f" Geometry points: {len(m['geom'])}")
print(f" First 3: {m['geom'][:3]}")
break
break
Wrote /work/examine.py (1559 chars).
9:08
Bash
cd /work && python examine.py
=== Current Innere Stadt ===
Tags: {
"admin_level": "9",
"boundary": "administrative",
"name": "Innere Stadt",
"name:uk": "\u0412\u043d\u0443\u0442\u0440\u0456\u0448\u043d\u0454 \u043c\u0456\u0441\u0442\u043e",
"population": "17135",
"ref": "1",
"ref:at:gkz": "90101",
"street_names:wikipedia": "de:Liste der Stra\u00dfennamen von Wien/Innere Stadt",
"type": "boundary",
"wikidata": "Q267329",
"wikipedia": "de:Innere Stadt (Wien)"
}
Members: 8
role=outer type=way ref=147416055 geom=False
role=outer type=way ref=147463458 geom=False
role=outer type=way ref=147396089 geom=False
role=outer type=way ref=147396086 geom=False
role=outer type=way ref=147396090 geom=False
=== 2014 Innere Stadt ===
Tags: {
"type": "boundary",
"ref:at:gkz": "90101",
"wikipedia:street_names:de": "Liste der Stra\u00dfennamen von Wien/Innere Stadt",
"ref": "1",
"wikipedia:de": "Innere Stadt (Wien)",
"wikipedia:en": "Innere Stadt",
"wikipedia:ru": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u0439 \u0433\u043e\u0440\u043e\u0434 (\u0412\u0435\u043d\u0430)",
"name": "Innere Stadt",
"boundary": "administrative",
"admin_level": "9",
"postal_code": "1010"
}
Members: 7
role=outer type=way ref=147399466 geom=False
role=outer type=way ref=147416055 geom=False
role=outer type=way ref=147463458 geom=False
role=outer type=way ref=147396089 geom=False
role=outer type=way ref=147396086 geom=False
9:14
The relations have members that are ways with role=outer, but the full geometry is not included in the response (geom=False). I used `out body geom` which should include geometry for nodes and ways... Let me check more carefully. Actually, the `out body geom;` at the end of the query returns geometry for the elements themselves (the relations), not necessarily for their member ways. I need to use `out geom;` or convert the way references to actual coordinates. Let me adjust - I need to either: 1. Use `(._;>;); out geom;` to recurse down and get full geometries 2. Or use `out body geom;` for relation + `recurse` to get ways with geom Let me try with a different query approach.
9:14
Write
/work/examine2.py
content · 27 lines · py
import json
with open('/work/current_districts.json') as f:
cur = json.load(f)
# Check if any element has geometry directly
for el in cur['elements']:
if el['type'] == 'relation' and el['id'] == 1990592:
# Check if 'geometry' key exists
print(f"Keys: {list(el.keys())}")
# The members
for m in el.get('members', []):
if m['role'] == 'outer':
print(f"Way ref={m['ref']}, has_geom={'geom' in m}")
break
# Check the whole response for any ways
ways = [el for el in cur['elements'] if el['type'] == 'way']
print(f"\nTotal ways in response: {len(ways)}")
if ways:
print(f"First way keys: {list(ways[0].keys())}")
print(f"First way nodes: {ways[0].get('nodes', [])[:5]}")
if 'geometry' in ways[0]:
print(f"Geometry: {ways[0]['geometry'][:3]}")
# Check if there's a 'geom' key (Overpass often calls it 'geometry')
if 'geom' in ways[0]:
print(f"geom: {ways[0]['geom'][:3]}")
Wrote /work/examine2.py (985 chars).
9:16
Bash
cd /work && python examine2.py
Keys: ['type', 'id', 'bounds', 'members', 'tags'] Way ref=147416055, has_geom=False Way ref=147463458, has_geom=False Way ref=147396089, has_geom=False Way ref=147396086, has_geom=False Way ref=147396090, has_geom=False Way ref=147399477, has_geom=False Way ref=147399466, has_geom=False Total ways in response: 0
9:28
The geometry data wasn't returned as full nodes/ways. The `out body geom` only returns geometry for nodes, not for relation members. I need a different approach. For Overpass, to get the full geometry of relations, I should use `(._;>;)` to recurse down to nodes, or use `out geom` which gives geometry for all elements including ways. Actually, `out body geom` should give coordinates for nodes AND ways (as geometry/geom). But the problem is that relations contain references to ways, and those ways need to be fetched too. The `out body geom` gives geometry for the relation members if we use the right syntax. Let me try `(._;>;)` after the relation query to get all child elements, then I can reconstruct the geometries. Actually for Overpass: - `out body` - gives the elements themselves - `out geom` - gives geometry for nodes and ways, but also for relation members if used with recursion - For relation members, we need the `geom` attribute on each member, which is only available with `out geom` or `out body geom` on the relation level, but I think the way geometries need to be resolved too. Wait, looking at the response more carefully - `out body geom` on a relation returns the relation with its members, and each member should have a `geom` array of coordinates. Let me check more carefully. Actually the doc says: "If you use the 'geom' parameter, the geometries of all elements and members will be added to the result." So with `out body geom`, relation members should have `geom` arrays. But from what I saw, the members don't have `geom` - they just have `ref`, `type`, and `role`. Hmm, maybe the issue is that `out body geom` only applies to ways and nodes directly, but relation members get geometry only when using `geom` on the recurse step. Let me try a different query approach: fetch the ways with geometry and reconstruct the relation. Actually, the simplest approach might be to use `out geom` (or `make` or `convert`) to get the full geometry. Or I could use `(._;>;)` to recurse and get all ways and nodes, then reconstruct. But actually, maybe the simplest is to use the Overpass API's built-in geometry output. Let me try: `rel(...); out geom;` - this should give the relation with geometry. Wait, I think the syntax might be different for getting geometry of relation members. Let me try: ``` [out:json]; rel(1990592); out geom; ``` This should give the relation and all its members with geometry data.
9:28
Write
/work/fetch_with_geom.py
content · 41 lines · py
import pycurl, io, json, urllib.parse
# Try different query styles to get geometry for relation members
# Style 1: out geom
query1 = '[out:json];rel(1990592);out geom;'
# Style 2: recurse then out
query2 = '[out:json];rel(1990592);(._;>;);out geom;'
for label, query in [("out geom", query1), ("recurse", query2)]:
buf = io.BytesIO()
c = pycurl.Curl()
c.setopt(c.URL, 'https://overpass-api.de/api/interpreter')
c.setopt(c.POSTFIELDS, 'data=' + urllib.parse.quote(query))
c.setopt(c.WRITEDATA, buf)
c.setopt(c.TIMEOUT, 30)
c.setopt(c.NOSIGNAL, 1)
c.perform()
status = c.getinfo(c.RESPONSE_CODE)
body = buf.getvalue().decode('utf-8')
print(f"\n=== {label} (status={status}) ===")
if status == 200:
obj = json.loads(body)
print(f"Elements: {len(obj.get('elements',[]))}")
for el in obj['elements']:
if el['type'] == 'relation':
print(f" Relation id={el['id']}")
for m in el.get('members', []):
print(f" Member: role={m['role']} type={m['type']} ref={m['ref']} geom={'geom' in m}")
elif el['type'] == 'way':
print(f" Way id={el['id']} nodes={len(el.get('nodes',[]))} geom={'geom' in el or 'geometry' in el}")
if 'geometry' in el:
print(f" Geometry points: {len(el['geometry'])} first: {el['geometry'][:2]}")
elif 'geom' in el:
print(f" geom points: {len(el['geom'])}")
elif el['type'] == 'node':
if len(obj['elements']) < 20:
print(f" Node id={el['id']} lat={el.get('lat')} lon={el.get('lon')}")
else:
print(body[:300])
Wrote /work/fetch_with_geom.py (1727 chars).
9:29
Bash
cd /work && python fetch_with_geom.py
=== out geom (status=200) ===
Elements: 1
Relation id=1990592
Member: role=outer type=way ref=147416055 geom=False
Member: role=outer type=way ref=147463458 geom=False
Member: role=outer type=way ref=147396089 geom=False
Member: role=outer type=way ref=147396086 geom=False
Member: role=outer type=way ref=147396090 geom=False
Member: role=outer type=way ref=147399477 geom=False
Member: role=outer type=way ref=147399466 geom=False
Member: role=label type=node ref=10919470469 geom=False
=== recurse (status=200) ===
Elements: 184
Way id=147396086 nodes=14 geom=True
Geometry points: 14 first: [{'lat': 48.2143408, 'lon': 16.3565102}, {'lat': 48.2142595, 'lon': 16.3564918}]
Way id=147396089 nodes=33 geom=True
Geometry points: 33 first: [{'lat': 48.2184891, 'lon': 16.370149}, {'lat': 48.2184449, 'lon': 16.370089}]
Way id=147396090 nodes=10 geom=True
Geometry points: 10 first: [{'lat': 48.2069081, 'lon': 16.3558875}, {'lat': 48.2058124, 'lon': 16.3573321}]
Way id=147399466 nodes=14 geom=True
Geometry points: 14 first: [{'lat': 48.1996821, 'lon': 16.3659409}, {'lat': 48.1995268, 'lon': 16.3659742}]
Way id=147399477 nodes=9 geom=True
Geometry points: 9 first: [{'lat': 48.2026474, 'lon': 16.3615382}, {'lat': 48.2025319, 'lon': 16.3617213}]
Way id=147416055 nodes=61 geom=True
Geometry points: 61 first: [{'lat': 48.1999528, 'lon': 16.375215}, {'lat': 48.2003186, 'lon': 16.3766526}]
Way id=147463458 nodes=41 geom=True
Geometry points: 41 first: [{'lat': 48.2113547, 'lon': 16.3848946}, {'lat': 48.2113936, 'lon': 16.3845071}]
Relation id=1990592
Member: role=outer type=way ref=147416055 geom=False
Member: role=outer type=way ref=147463458 geom=False
Member: role=outer type=way ref=147396089 geom=False
Member: role=outer type=way ref=147396086 geom=False
Member: role=outer type=way ref=147396090 geom=False
Member: role=outer type=way ref=147399477 geom=False
Member: role=outer type=way ref=147399466 geom=False
Member: role=label type=node ref=10919470469 geom=False
9:38
So the `out geom` (without recursion `>`) only gives the relation itself. We need `(._;>;); out geom;` to get the ways with full geometry. Let me now build the full solution. The approach: 1. Get current districts with `rel(ids);(._;>;);out geom;` 2. Get 2014 districts similarly with `[date:"2014-01-01T00:00:00Z"]` 3. For each relation, extract the outer ways' geometries, construct polygons 4. Build union of all districts for each snapshot 5. Compute symmetric difference (added/removed areas) 6. For each district, check if it borders any changed area 7. Output GeoJSON Let me plan the GeoJSON structure: - change_type: "added_since_2014", "removed_since_2014", or "unchanged" - district_name: normalised name - touches_changed: boolean The geometry fragments should be individual features, each with those properties. Let me write a comprehensive script.
9:38
9:38
Write
/work/fetch_full_current.py
content · 37 lines · py
import pycurl, io, json, urllib.parse
ids = [1990590, 1990591, 1990592, 1990593, 1990594, 1990595, 1990596,
1990597, 1990598, 1990599, 1990600, 1991416, 1991433, 1991434,
1991435, 1991436, 1991437, 1991438, 1991439, 1991440, 1991441,
1991442, 1991443]
rel_ids = ','.join(str(i) for i in ids)
# Query: get relations, recurse to ways with geometry
query = f'[out:json];rel({rel_ids});(._;>;);out geom;'
buf = io.BytesIO()
c = pycurl.Curl()
c.setopt(c.URL, 'https://overpass-api.de/api/interpreter')
c.setopt(c.POSTFIELDS, 'data=' + urllib.parse.quote(query))
c.setopt(c.WRITEDATA, buf)
c.setopt(c.TIMEOUT, 300)
c.setopt(c.NOSIGNAL, 1)
c.perform()
status = c.getinfo(c.RESPONSE_CODE)
body = buf.getvalue().decode('utf-8')
print(f"Status: {status}, Length: {len(body)}")
if status == 200:
obj = json.loads(body)
print(f"Elements: {len(obj.get('elements',[]))}")
with open('/work/current_full.json', 'w') as f:
json.dump(obj, f)
print("Saved current_full.json")
# Count types
types = {}
for el in obj['elements']:
t = el['type']
types[t] = types.get(t, 0) + 1
print(f"Types: {types}")
else:
print(body[:500])
Wrote /work/fetch_full_current.py (1195 chars).
9:39
Bash
cd /work && timeout 120 python fetch_full_current.py
Status: 400, Length: 1105
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" lang="en"/>
<title>OSM3S Response</title>
</head>
<body>
<p>The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.</p>
<p><strong style="color:#FF0000
9:46
The query with too many relation IDs at once is causing a parsing error. Let me do it in smaller batches or one by one.
9:46
Write
/work/fetch_full_batch.py
content · 44 lines · py
import pycurl, io, json, urllib.parse, time
ids = [1990590, 1990591, 1990592, 1990593, 1990594, 1990595, 1990596,
1990597, 1990598, 1990599, 1990600, 1991416, 1991433, 1991434,
1991435, 1991436, 1991437, 1991438, 1991439, 1991440, 1991441,
1991442, 1991443]
def fetch_batch(rids, label):
rel_ids = ','.join(str(i) for i in rids)
query = f'[out:json];rel({rel_ids});(._;>;);out geom;'
buf = io.BytesIO()
c = pycurl.Curl()
c.setopt(c.URL, 'https://overpass-api.de/api/interpreter')
c.setopt(c.POSTFIELDS, 'data=' + urllib.parse.quote(query))
c.setopt(c.WRITEDATA, buf)
c.setopt(c.TIMEOUT, 120)
c.setopt(c.NOSIGNAL, 1)
c.perform()
status = c.getinfo(c.RESPONSE_CODE)
body = buf.getvalue().decode('utf-8')
print(f" {label}: status={status}, length={len(body)}")
if status == 200:
return json.loads(body)
else:
print(f" Error: {body[:300]}")
return None
all_data = {"version": 0.6, "generator": "Overpass", "elements": []}
# Try batches of 5
batch_size = 5
for i in range(0, len(ids), batch_size):
batch = ids[i:i+batch_size]
label = f"ids {batch[0]}-{batch[-1]}"
print(f"Fetching {label}...")
data = fetch_batch(batch, label)
if data:
all_data['elements'].extend(data['elements'])
time.sleep(3)
print(f"\nTotal elements collected: {len(all_data['elements'])}")
with open('/work/current_full.json', 'w') as f:
json.dump(all_data, f)
print("Saved current_full.json")
Wrote /work/fetch_full_batch.py (1511 chars).
9:48
Bash
cd /work && timeout 300 python fetch_full_batch.py
Fetching ids 1990590-1990594...
ids 1990590-1990594: status=400, length=1105
Error: <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" lan
Fetching ids 1990595-1990599...
ids 1990595-1990599: status=400, length=1105
Error: <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" lan
Fetching ids 1990600-1991435...
ids 1990600-1991435: status=400, length=1105
Error: <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" lan
Fetching ids 1991436-1991440...
ids 1991436-1991440: status=400, length=1105
Error: <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" lan
Fetching ids 1991441-1991443...
Traceback (most recent call last):
File "/work/fetch_full_batch.py", line 36, in <module>
data = fetch_batch(batch, label)
File "/work/fetch_full_batch.py", line 18, in fetch_batch
c.perform()
~~~~~~~~~^^
pycurl.error: (7, 'Failed to connect to overpass-api.de port 443 after 89 ms: Could not connect to server')
10:08
The server is rate-limiting me. Let me try a different approach - fetch each district individually with sleeping in between.
10:08
10:08
Write
/work/fetch_full_slow.py
content · 56 lines · py
import pycurl, io, json, urllib.parse, time
ids = [1990590, 1990591, 1990592, 1990593, 1990594, 1990595, 1990596,
1990597, 1990598, 1990599, 1990600, 1991416, 1991433, 1991434,
1991435, 1991436, 1991437, 1991438, 1991439, 1991440, 1991441,
1991442, 1991443]
all_elements = []
for i, rid in enumerate(ids):
query = f'[out:json];rel({rid});(._;>;);out geom;'
buf = io.BytesIO()
c = pycurl.Curl()
c.setopt(c.URL, 'https://overpass-api.de/api/interpreter')
c.setopt(c.POSTFIELDS, 'data=' + urllib.parse.quote(query))
c.setopt(c.WRITEDATA, buf)
c.setopt(c.TIMEOUT, 60)
c.setopt(c.NOSIGNAL, 1)
c.perform()
status = c.getinfo(c.RESPONSE_CODE)
body = buf.getvalue().decode('utf-8')
tags = {}
if status == 200:
data = json.loads(body)
for el in data['elements']:
all_elements.append(el)
if el['type'] == 'relation':
tags = el.get('tags', {})
name = tags.get('name', '?')
print(f" [{i+1}/{len(ids)}] id={rid} name={name} status=200 elements={len(data['elements'])}")
else:
print(f" [{i+1}/{len(ids)}] id={rid} status={status}")
if status == 429:
print(" Rate limited, waiting 10s...")
time.sleep(10)
# Retry
c.perform()
status2 = c.getinfo(c.RESPONSE_CODE)
body2 = buf.getvalue().decode('utf-8')
if status2 == 200:
data = json.loads(body2)
for el in data['elements']:
all_elements.append(el)
if el['type'] == 'relation':
tags = el.get('tags', {})
name = tags.get('name', '?')
print(f" Retry: id={rid} name={name} status=200 elements={len(data['elements'])}")
# Be nice to server
time.sleep(5)
print(f"\nTotal elements: {len(all_elements)}")
with open('/work/current_full.json', 'w') as f:
json.dump({"version": 0.6, "generator": "Overpass", "elements": all_elements}, f)
print("Saved current_full.json")
Wrote /work/fetch_full_slow.py (2103 chars).
10:14
Actually, instead of fetching one-by-one with pycurl which is slow, let me use a different approach. I can use the existing current_districts.json which has the relation structures with member references, along with the way/node data. But the issue is that `current_districts.json` doesn't have geometry for the ways. Let me think about what I need. I need to build proper multipolygon geometries for each district from both dates, then compute the symmetric difference. A better approach: 1. Use the Overpass API to get the full geometry in one call per district (using the recursed query we tested that works for one relation at a time) 2. Or use a different Overpass server 3. Or use the Overpass Turbo approach Actually, the issue with the batch query is the area filter - wait, no, I'm not using an area filter. The error was about bbox-query. Let me check the actual error message.
10:14
Write
/work/fetch_error_check.py
content · 20 lines · py
import pycurl, io, json, urllib.parse
# Check full error for batch of 5
ids = [1990590, 1990591, 1990592, 1990593, 1990594]
rel_ids = ','.join(str(i) for i in ids)
query = f'[out:json];rel({rel_ids});(._;>;);out geom;'
print(f"Query: {query}")
buf = io.BytesIO()
c = pycurl.Curl()
c.setopt(c.URL, 'https://overpass-api.de/api/interpreter')
c.setopt(c.POSTFIELDS, 'data=' + urllib.parse.quote(query))
c.setopt(c.WRITEDATA, buf)
c.setopt(c.TIMEOUT, 60)
c.setopt(c.NOSIGNAL, 1)
c.perform()
status = c.getinfo(c.RESPONSE_CODE)
body = buf.getvalue().decode('utf-8')
print(f"Status: {status}")
print(f"Full body:\n{body}")
Wrote /work/fetch_error_check.py (618 chars).
10:16
Bash
cd /work && python fetch_error_check.py
Query: [out:json];rel(1990590,1990591,1990592,1990593,1990594);(._;>;);out geom;
Traceback (most recent call last):
File "/work/fetch_error_check.py", line 16, in <module>
c.perform()
~~~~~~~~~^^
pycurl.error: (7, 'Failed to connect to overpass-api.de port 443 after 90 ms: Could not connect to server')
10:18
The server seems to be rate-limiting us heavily. Let me wait a bit and try a single, simple request first.
10:18
Bash
sleep 30 && cd /work && python fetch_error_check.py
Query: [out:json];rel(1990590,1990591,1990592,1990593,1990594);(._;>;);out geom;
Traceback (most recent call last):
File "/work/fetch_error_check.py", line 16, in <module>
c.perform()
~~~~~~~~~^^
pycurl.error: (7, 'Failed to connect to overpass-api.de port 443 after 111 ms: Could not connect to server')
10:55
The Overpass API seems to be timing out on connection now. Let me check if we have the data already saved and can work with what we have. We already have: 1. `current_districts.json` - 23 districts with member info (but no way/node geometries) - this has relations with member refs but no geometry 2. `districts_2014.json` - 23 districts with member info (no way/node geometries) Let me check what we have more carefully and see if we can reconstruct geometries from the data we have. Actually, we already successfully fetched the full recurse data for one district (1990592) in the earlier test. Let me check if `current_full.json` got partially saved... Actually no, the batch approach failed. Wait - we have `current_districts.json` which has 665643 bytes. Let me check if it has way elements with geometry. Actually, looking back at the earlier test, the `out body geom;` query returned only 23 elements (all relations) and no way geometry. The `(._;>;);out geom;` query returns ways with geometry. Let me try a different approach - maybe I can use the node coordinates from the relation members that have geom. But looking at the earlier results, `out body geom` on a relation doesn't give geometry for the member ways. Let me try to fetch one district at a time more carefully.
10:55
Write
/work/fetch_one.py
content · 29 lines · py
import pycurl, io, json, urllib.parse, time
query = '[out:json];rel(1990590);(._;>;);out geom;'
print(f"Query: {query}")
buf = io.BytesIO()
c = pycurl.Curl()
c.setopt(c.URL, 'https://overpass-api.de/api/interpreter')
c.setopt(c.POSTFIELDS, 'data=' + urllib.parse.quote(query))
c.setopt(c.WRITEDATA, buf)
c.setopt(c.TIMEOUT, 60)
c.setopt(c.NOSIGNAL, 1)
c.setopt(c.FRESH_CONNECT, 1)
c.setopt(c.FORBID_REUSE, 1)
c.perform()
status = c.getinfo(c.RESPONSE_CODE)
body = buf.getvalue().decode('utf-8')
print(f"Status: {status}, Length: {len(body)}")
if status == 200:
data = json.loads(body)
print(f"Elements: {len(data['elements'])}")
# Count types
types = {}
for el in data['elements']:
t = el['type']
types[t] = types.get(t, 0) + 1
print(f"Types: {types}")
else:
print(body[:500])
Wrote /work/fetch_one.py (822 chars).
10:57
Bash
cd /work && python fetch_one.py
Query: [out:json];rel(1990590);(._;>;);out geom;
Traceback (most recent call last):
File "/work/fetch_one.py", line 15, in <module>
c.perform()
~~~~~~~~~^^
pycurl.error: (7, 'Failed to connect to overpass-api.de port 443 after 93 ms: Could not connect to server')
11:03
The server seems to be down right now. But I already have the current data with way refs from the first fetch (current_districts.json) and the 2014 data with way refs (districts_2014.json). Actually, I have the relation-level data without the way geometries from those files. But the earlier one-by-one fetches (fetch_2014_batch.py) worked - those fetched single relations with `out body geom` but the geom was only on relation members, not on ways. The ways had `geom=False`. Let me check if the already fetched files have any usable geometry. Actually no - they were fetched with `out body geom` on the relation which gives bounds but not way geometries. Wait, let me check what we actually have in the files.
11:03
Write
/work/check_data.py
content · 25 lines · py
import json
# Check what geometry data we already have
for fname, label in [('/work/current_districts.json', 'current'), ('work/districts_2014.json', '2014')]:
try:
with open(fname) as f:
data = json.load(f)
print(f"\n=== {label} ===")
print(f"Elements: {len(data['elements'])}")
for el in data['elements']:
if el['type'] == 'relation':
tags = el.get('tags', {})
name = tags.get('name', '?')
mems = el.get('members', [])
outer_count = sum(1 for m in mems if m['role'] == 'outer')
has_geom_members = any('geom' in m for m in mems)
print(f" id={el['id']} name={name} outer_ways={outer_count} has_member_geom={has_geom_members}")
if has_geom_members:
for m in mems:
if 'geom' in m:
print(f" First member with geom: role={m['role']} pts={len(m['geom'])}")
print(f" First pt: {m['geom'][0]}")
break
except FileNotFoundError:
print(f"\nFile not found: {fname}")
Wrote /work/check_data.py (1175 chars).
11:07
The current_districts.json was fetched with `out body geom;` which doesn't include way geometry - just the relation metadata. The `districts_2014.json` was fetched the same way. I need to get the way geometries. The `out geom;` query on a relation with recurse `(._;>;)` gets the ways with their geometry lists. But the server seems to be blocking me now. Let me try a different approach. Since the initial queries worked with `out body geom;` (which gives member geometry on relation members if available), let me check what the `out body geom` actually returned - maybe it already has member geometry. Actually wait, looking at the first test: - `out body geom;` gave members with `geom=False` - so no geometry on members - `out geom;` with recurse gave ways with `geom=True` But the server seems overloaded now. Let me wait and try again. Actually, let me think about this differently. I already have the data files with the relation member references. The Overpass server is having issues now but was working before. Let me try again later. Actually wait, the error is "Failed to connect to overpass-api.de port 443 after 90 ms: Could not connect to server" - this suggests a DNS/network issue, not rate limiting. Let me check if overpass-api.de resolves.
11:07
Bash
python -c "import socket; print(socket.getaddrinfo('overpass-api.de', 443))"
[(<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('162.55.144.139', 443)), (<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_DGRAM: 2>, 17, '', ('162.55.144.139', 443)), (<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_RAW: 3>, 0, '', ('162.55.144.139', 443)), (<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('65.109.112.52', 443)), (<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_DGRAM: 2>, 17, '', ('65.109.112.52', 443)), (<AddressFamily.AF_INET: 2>, <SocketKind.SOCK_RAW: 3>, 0, '', ('65.109.112.52', 443)), (<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('2a01:4f8:261:3c4f::2', 443, 0, 0)), (<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_DGRAM: 2>, 17, '', ('2a01:4f8:261:3c4f::2', 443, 0, 0)), (<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_RAW: 3>, 0, '', ('2a01:4f8:261:3c4f::2', 443, 0, 0)), (<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('2a01:4f9:3051:3e48::2', 443, 0, 0)), (<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_DGRAM: 2>, 17, '', ('2a01:4f9:3051:3e48::2', 443, 0, 0)), (<AddressFamily.AF_INET6: 10>, <SocketKind.SOCK_RAW: 3>, 0, '', ('2a01:4f9:3051:3e48::2', 443, 0, 0))]
11:13
The DNS resolution works, but connection is failing. Let me look at what data we already have. The current_districts.json has 23 relations but their members don't have geometry. The initial query `out body geom` on relations returns the relation with member refs but no way/node geometries. We need `(._;>;);out geom;` to recurse into ways, but that's failing now. Let me check if we already have the geometry data we need from the earlier successful fetch_test10.py. Actually, looking back, the current_districts.json was fetched with `out body geom` on just the relations, which doesn't give way geometries. But the 2014 data was fetched one-by-one with `out body geom` on each relation, also without recursing into ways. So neither current nor 2014 snapshots have the way-level geometries we need to build polygons. Let me think of an alternative approach. We already have the current data with just relation members (way references but no geometry). We need to either: 1. Fetch ways with geometry separately 2. Use a different query approach The Overpass server seems to be intermittently blocking us. Let me try again with a different approach. Actually, wait - the test earlier with `out geom` on single relation worked: - `[out:json];rel(1990590);out body geom;` worked and returned 1 element (the relation) - `[out:json];rel(1990590);(._;>;);out geom;` worked earlier and returned 184 elements (relation + ways + nodes) with way geometry The issue now seems like the server is rate-limiting or blocking our IP. Let me try with different approach.
11:13
Bash
sleep 20 && cd /work && python fetch_one.py
Query: [out:json];rel(1990590);(._;>;);out geom;
Traceback (most recent call last):
File "/work/fetch_one.py", line 15, in <module>
c.perform()
~~~~~~~~~^^
pycurl.error: (7, 'Failed to connect to overpass-api.de port 443 after 109 ms: Could not connect to server')
11:40
The server seems to be temporarily blocking me. Let me check what data we already have. The original `current_districts.json` and `districts_2014.json` were fetched with `out body geom` and they had the geometry in the `geom` field of members. Let me check more carefully.
11:40
Write
/work/check_geom.py
content · 44 lines · py
import json
# Check if members have 'geom' key in the data we fetched
with open('/work/current_districts.json') as f:
cur = json.load(f)
with open('/work/districts_2014.json') as f:
old = json.load(f)
# Check current
print("=== Current ===")
for el in cur['elements']:
if el['type'] == 'relation':
name = el.get('tags', {}).get('name', '?')
mems = el.get('members', [])
for m in mems:
if 'geom' in m:
print(f" id={el['id']} name={name}: member type={m['type']} ref={m['ref']} geom_pts={len(m['geom'])}")
break
else:
print(f" id={el['id']} name={name}: NO geometry on members")
# Check if 'geometry' key exists
for m in mems:
if 'geometry' in m:
print(f" -> Found 'geometry' on member!")
break
else:
print(f" -> Member keys sample: {list(mems[0].keys()) if mems else 'no members'}")
# Check if relation itself has geometry
if 'geometry' in el:
print(f" -> Relation has geometry key!")
if 'geom' in el:
print(f" -> Relation has geom key!")
print("\n=== 2014 ===")
for el in old['elements']:
if el['type'] == 'relation':
name = el.get('tags', {}).get('name', '?')
mems = el.get('members', [])
for m in mems:
if 'geom' in m:
print(f" id={el['id']} name={name}: member type={m['type']} ref={m['ref']} geom_pts={len(m['geom'])}")
break
else:
print(f" id={el['id']} name={name}: NO geometry on members")
Wrote /work/check_geom.py (1703 chars).
11:46
Let me check if the data we already fetched (current_districts.json, districts_2014.json) has geometry on the members. The earlier test showed `geom=False` for members, meaning `out body geom` only gives member references without geometry. We need the recurse approach to get way geometries, but the server is currently unresponsive due to rate limiting.
Let me think about what data we have and how to proceed. The current_districts.json and districts_2014.json have the relations with member lists (way references), but the actual way geometries weren't fetched because `out body geom` for relations gives just the relation metadata and member references.
Wait, looking back at the first fetch_test5.py run, the response had `geom=True` on members... Let me re-check.
Actually, in `examine.py` output:
```
Member with geom: role=outer type=way ref=147416055 geom=True
Geometry points: 61
```
Wait, that was from the first batch that actually worked (fetch_test5.py). And in `examine2.py`:
```
Way ref=147416055, has_geom=False
```
Hmm, so there's a discrepancy. Let me look more carefully.
In `examine.py`, the output was:
```
Members: 8
role=outer type=way ref=147416055 geom=False
```
But then:
```
for m in mems:
if 'geom' in m:
```
That found one. So the first few members didn't have 'geom' but one of them deeper in the list did. Let me re-examine.
Actually wait, looking at examine2.py output again:
```
Keys: ['type', 'id', 'bounds', 'members', 'tags']
Way ref=147416055, has_geom=False
...
Total ways in response: 0
```
So the `current_districts.json` file that we saved from the first successful fetch already has geometry in the member `geom` field! Let me just verify this.
11:46
Bash
cd /work && python check_geom.py
=== Current ===
id=1990590 name=Alsergrund: NO geometry on members
-> Found 'geometry' on member!
id=1990591 name=Hietzing: NO geometry on members
-> Found 'geometry' on member!
id=1990592 name=Innere Stadt: NO geometry on members
-> Found 'geometry' on member!
id=1990593 name=Josefstadt: NO geometry on members
-> Found 'geometry' on member!
id=1990594 name=Leopoldstadt: NO geometry on members
-> Found 'geometry' on member!
id=1990595 name=Mariahilf: NO geometry on members
-> Found 'geometry' on member!
id=1990596 name=Meidling: NO geometry on members
-> Found 'geometry' on member!
id=1990597 name=Neubau: NO geometry on members
-> Found 'geometry' on member!
id=1990598 name=Penzing: NO geometry on members
-> Found 'geometry' on member!
id=1990599 name=Rudolfsheim-Fünfhaus: NO geometry on members
-> Found 'geometry' on member!
id=1990600 name=Währing: NO geometry on members
-> Found 'geometry' on member!
id=1991416 name=Landstraße: NO geometry on members
-> Found 'geometry' on member!
id=1991433 name=Brigittenau: NO geometry on members
-> Found 'geometry' on member!
id=1991434 name=Donaustadt: NO geometry on members
-> Found 'geometry' on member!
id=1991435 name=Döbling: NO geometry on members
-> Found 'geometry' on member!
id=1991436 name=Favoriten: NO geometry on members
-> Found 'geometry' on member!
id=1991437 name=Floridsdorf: NO geometry on members
-> Found 'geometry' on member!
id=1991438 name=Hernals: NO geometry on members
-> Found 'geometry' on member!
id=1991439 name=Liesing: NO geometry on members
-> Found 'geometry' on member!
id=1991440 name=Margareten: NO geometry on members
-> Found 'geometry' on member!
id=1991441 name=Ottakring: NO geometry on members
-> Found 'geometry' on member!
id=1991442 name=Simmering: NO geometry on members
-> Found 'geometry' on member!
id=1991443 name=Wieden: NO geometry on members
-> Found 'geometry' on member!
=== 2014 ===
id=1990590 name=Alsergrund: NO geometry on members
id=1990591 name=Hietzing: NO geometry on members
id=1990592 name=Innere Stadt: NO geometry on members
id=1990593 name=Josefstadt: NO geometry on members
id=1990594 name=Leopoldstadt: NO geometry on members
id=1990595 name=Mariahilf: NO geometry on members
id=1990596 name=Meidling: NO geometry on members
id=1990597 name=Neubau: NO geometry on members
id=1990598 name=Penzing: NO geometry on members
id=1990599 name=Rudolfsheim-Fünfhaus: NO geometry on members
id=1990600 name=Währing: NO geometry on members
id=1991416 name=Landstraße: NO geometry on members
id=1991433 name=Brigittenau: NO geometry on members
id=1991434 name=Donaustadt: NO geometry on members
id=1991435 name=Döbling: NO geometry on members
id=1991436 name=Favoriten: NO geometry on members
id=1991437 name=Floridsdorf: NO geometry on members
id=1991438 name=Hernals: NO geometry on members
id=1991439 name=Liesing: NO geometry on members
id=1991440 name=Margareten: NO geometry on members
id=1991441 name=Ottakring: NO geometry on members
id=1991442 name=Simmering: NO geometry on members
id=1991443 name=Wieden: NO geometry on members
11:54
So the current data has 'geometry' key on members (not 'geom'), while the 2014 data doesn't have geometry at all because we used `out body geom;` which gives member geometry only for the current snapshot. For the 2014 data we also used `out body geom;` but it might not have returned member geometries because... hmm, let me check more carefully. Actually, looking at the initial current data query `[out:json];area["name"="Wien"]->.a;rel(area.a)[admin_level=9][boundary=administrative];out body geom;` - this returned 23 relations with NO geometry on members either! But wait, the earlier check showed `Has 'geometry' on member!` for current. So maybe the `out body geom;` returns geometry only when you add `(._;>;);` before out. Let me look more carefully at the 2014 data to see if the members have 'geometry' key.
11:54
Write
/work/check_geom2.py
content · 44 lines · py
import json
# Check geometry details
with open('/work/current_districts.json') as f:
cur = json.load(f)
with open('/work/districts_2014.json') as f:
old = json.load(f)
# Check first member of first relation in current
el = cur['elements'][0]
print(f"Current first element: id={el['id']} type={el['type']}")
mems = el.get('members', [])
if mems:
m = mems[0]
print(f" First member keys: {list(m.keys())}")
print(f" type={m['type']} ref={m['ref']} role={m['role']}")
if 'geometry' in m:
print(f" geometry len: {len(m['geometry'])}")
print(f" first pt: {m['geometry'][0]}")
# Check 2014
el = old['elements'][0]
print(f"\n2014 first element: id={el['id']} type={el['type']}")
mems = el.get('members', [])
if mems:
m = mems[0]
print(f" First member keys: {list(m.keys())}")
print(f" type={m['type']} ref={m['ref']} role={m['role']}")
# Let's check all 2014 members for a single district
for el in old['elements']:
if el['id'] == 1990590:
print(f"\n2014 Alsergrund members:")
for m in el.get('members', []):
print(f" type={m['type']} ref={m['ref']} role={m['role']} keys={list(m.keys())}")
break
# And for current
for el in cur['elements']:
if el['id'] == 1990590:
print(f"\nCurrent Alsergrund members:")
for m in el.get('members', []):
print(f" type={m['type']} ref={m['ref']} role={m['role']} keys={list(m.keys())}")
break
Wrote /work/check_geom2.py (1464 chars).
11:56
So the data was fetched with `out body geom;` and the members have 'geometry' key (not 'geom'). This is the Overpass geometry format where each member has a 'geometry' array of {lat, lon} objects. Let me check how to build polygon geometries from this.
Also for the 2014 data, I need to check if members have 'geometry' key too - they might not since I fetched them differently.
11:56
Bash
cd /work && python check_geom2.py
Current first element: id=1990590 type=relation
First member keys: ['type', 'ref', 'role', 'geometry']
type=way ref=147468285 role=outer
geometry len: 32
first pt: {'lat': 48.2151672, 'lon': 16.3408703}
2014 first element: id=1990590 type=relation
First member keys: ['type', 'ref', 'role', 'geometry']
type=way ref=147468285 role=outer
2014 Alsergrund members:
type=way ref=147468285 role=outer keys=['type', 'ref', 'role', 'geometry']
type=way ref=8046654 role=outer keys=['type', 'ref', 'role', 'geometry']
type=way ref=104962880 role=outer keys=['type', 'ref', 'role', 'geometry']
type=way ref=68480634 role=outer keys=['type', 'ref', 'role', 'geometry']
type=way ref=147396089 role=outer keys=['type', 'ref', 'role', 'geometry']
type=way ref=147463455 role=outer keys=['type', 'ref', 'role', 'geometry']
type=way ref=147463456 role=outer keys=['type', 'ref', 'role', 'geometry']
type=way ref=147463454 role=outer keys=['type', 'ref', 'role', 'geometry']
type=way ref=147463462 role=outer keys=['type', 'ref', 'role', 'geometry']
type=way ref=206221053 role=outer keys=['type', 'ref', 'role', 'geometry']
Current Alsergrund members:
type=way ref=147468285 role=outer keys=['type', 'ref', 'role', 'geometry']
type=way ref=8046654 role=outer keys=['type', 'ref', 'role', 'geometry']
type=way ref=147396089 role=outer keys=['type', 'ref', 'role', 'geometry']
type=way ref=147463455 role=outer keys=['type', 'ref', 'role', 'geometry']
type=way ref=147463456 role=outer keys=['type', 'ref', 'role', 'geometry']
type=way ref=390540126 role=outer keys=['type', 'ref', 'role', 'geometry']
type=way ref=390540129 role=outer keys=['type', 'ref', 'role', 'geometry']
type=way ref=147463454 role=outer keys=['type', 'ref', 'role', 'geometry']
type=way ref=147463462 role=outer keys=['type', 'ref', 'role', 'geometry']
12:03
So both datasets have 'geometry' on members. The 2014 data has `geometry` key on members too. Let me check the 2014 more closely - it seems the first member didn't show geometry because I printed the wrong thing. Let me check again. Actually looking more carefully: - Current first member shows `geometry` key with 32 points - 2014 first member shows keys=['type', 'ref', 'role', 'geometry'] - so it DOES have geometry! Great, so both datasets have geometry. Now I need to: 1. Build polygon geometries from the outer way members for each district 2. Normalize district names 3. Find changed areas between the two snapshots 4. Classify them Let me check the 2014 geometry more carefully.
12:03
Write
/work/check_geom3.py
content · 27 lines · py
import json
with open('/work/current_districts.json') as f:
cur = json.load(f)
with open('/work/districts_2014.json') as f:
old = json.load(f)
# Check 2014 geometry data
for el in old['elements']:
if el['id'] == 1990590:
for m in el.get('members', []):
if 'geometry' in m:
print(f"2014: type={m['type']} ref={m['ref']} role={m['role']} geometry_pts={len(m['geometry'])}")
print(f" first pt: {m['geometry'][0]}")
break
break
# Check if 2014 data has geometry for all
for el in old['elements']:
if el['type'] == 'relation':
name = el.get('tags', {}).get('name', '?')
mems = el.get('members', [])
has_geom = any('geometry' in m for m in mems if m['role'] == 'outer')
if not has_geom:
print(f"2014: {name} has NO geometry on outer ways!")
else:
print(f"2014: {name} has geometry OK")
Wrote /work/check_geom3.py (939 chars).
12:04
Bash
cd /work && python check_geom3.py
2014: type=way ref=147468285 role=outer geometry_pts=17
first pt: {'lat': 48.2151672, 'lon': 16.3408703}
2014: Alsergrund has geometry OK
2014: Hietzing has geometry OK
2014: Innere Stadt has geometry OK
2014: Josefstadt has geometry OK
2014: Leopoldstadt has geometry OK
2014: Mariahilf has geometry OK
2014: Meidling has geometry OK
2014: Neubau has geometry OK
2014: Penzing has geometry OK
2014: Rudolfsheim-Fünfhaus has geometry OK
2014: Währing has geometry OK
2014: Landstraße has geometry OK
2014: Brigittenau has geometry OK
2014: Donaustadt has geometry OK
2014: Döbling has geometry OK
2014: Favoriten has geometry OK
2014: Floridsdorf has geometry OK
2014: Hernals has geometry OK
2014: Liesing has geometry OK
2014: Margareten has geometry OK
2014: Ottakring has geometry OK
2014: Simmering has geometry OK
2014: Wieden has geometry OK
12:10
Now I have both datasets with geometry data. Let me look at the actual geometry keys more carefully and also check for any name differences between the datasets. The current data has "geometry" on members (as seen in check_geom2.py). The 2014 data also has "geometry" on members. Good. Now the plan: 1. Parse both datasets - for each district, extract the outer way geometries and build a polygon 2. Normalise district names (handle casing/spelling differences) 3. Union all current polygons and all 2014 polygons 4. Compute the symmetric difference to find what's changed 5. Classify fragments as added_since_2014 (in current but not 2014), removed_since_2014 (in 2014 but not current), or unchanged 6. For each district, flag whether its geometry touches any area that was added or removed Let me check the name differences first.
12:10
12:10
Write
/work/check_names.py
content · 16 lines · py
import json
with open('/work/current_districts.json') as f:
cur = json.load(f)
with open('/work/districts_2014.json') as f:
old = json.load(f)
print("=== Current names ===")
for el in sorted(cur['elements'], key=lambda x: int(x.get('tags',{}).get('ref', '99'))):
tags = el.get('tags', {})
print(f" ref={tags.get('ref','?')} name='{tags.get('name','?')}'")
print("\n=== 2014 names ===")
for el in sorted(old['elements'], key=lambda x: int(x.get('tags',{}).get('ref', '99'))):
tags = el.get('tags', {})
print(f" ref={tags.get('ref','?')} name='{tags.get('name','?')}'")
Wrote /work/check_names.py (596 chars).
12:11
Bash
cd /work && python check_names.py
=== Current names === ref=1 name='Innere Stadt' ref=2 name='Leopoldstadt' ref=3 name='Landstraße' ref=4 name='Wieden' ref=5 name='Margareten' ref=6 name='Mariahilf' ref=7 name='Neubau' ref=8 name='Josefstadt' ref=9 name='Alsergrund' ref=10 name='Favoriten' ref=11 name='Simmering' ref=12 name='Meidling' ref=13 name='Hietzing' ref=14 name='Penzing' ref=15 name='Rudolfsheim-Fünfhaus' ref=16 name='Ottakring' ref=17 name='Hernals' ref=18 name='Währing' ref=19 name='Döbling' ref=20 name='Brigittenau' ref=21 name='Floridsdorf' ref=22 name='Donaustadt' ref=23 name='Liesing' === 2014 names === ref=1 name='Innere Stadt' ref=2 name='Leopoldstadt' ref=3 name='Landstraße' ref=4 name='Wieden' ref=5 name='Margareten' ref=6 name='Mariahilf' ref=7 name='Neubau' ref=8 name='Josefstadt' ref=9 name='Alsergrund' ref=10 name='Favoriten' ref=11 name='Simmering' ref=12 name='Meidling' ref=13 name='Hietzing' ref=14 name='Penzing' ref=15 name='Rudolfsheim-Fünfhaus' ref=16 name='Ottakring' ref=17 name='Hernals' ref=18 name='Währing' ref=19 name='Döbling' ref=20 name='Brigittenau' ref=21 name='Floridsdorf' ref=22 name='Donaustadt' ref=23 name='Liesing'
12:43
The names look the same. The "casing and spelling" differences mentioned might refer to older data, but in 2014 the names are already the same as current. Let me look more carefully at the tags - maybe there are extra names. Actually, the user said "District names changed casing and spelling between snapshots" - so let me try to match by ref number, which is a reliable district identifier. Let me also look at the actual geometry changes. Now I have a plan: 1. Build district polygons from both snapshots using the member way geometries (outer ways form the boundary ring) 2. Match districts by ref number (since names should be normalised) 3. Compute symmetric difference to find changed areas 4. Classify fragments Let me write the main processing script.
12:43
12:43
Write
/work/solve.py
content · 379 lines · py
import json
from shapely.geometry import Polygon, MultiPolygon, shape, mapping
from shapely.ops import unary_union
import geopandas as gpd
def build_polygon_from_relation(rel):
"""Build a Polygon from a relation's outer way geometries."""
outer_rings = []
for m in rel.get('members', []):
if m['role'] == 'outer' and 'geometry' in m:
coords = [(pt['lon'], pt['lat']) for pt in m['geometry']]
if len(coords) >= 3:
outer_rings.append(coords)
if not outer_rings:
return None
# Try to merge into a single polygon (most districts have 1 outer ring)
# For districts with multiple outer ways, we need to assemble them
# First, collect all rings as linestrings
from shapely.geometry import LineString
from shapely.ops import polygonize
lines = [LineString(ring) for ring in outer_rings]
# Try simple approach: if the first and last points match, it's a closed ring
# Use polygonize to merge the lines
merged = unary_union(lines)
if merged.geom_type == 'LineString':
# Single line - try to close it
if merged.is_ring:
return Polygon(merged.coords)
else:
print(f" WARNING: merged line is not a ring for rel {rel['id']}")
return None
elif merged.geom_type == 'MultiLineString':
# Try polygonizing
polygons = list(polygonize(merged.geoms))
if len(polygons) == 1:
return polygons[0]
elif len(polygons) > 1:
return MultiPolygon(polygons)
else:
print(f" WARNING: cannot polygonize for rel {rel['id']}")
return None
else:
return None
def build_polygon_simple(rel):
"""
Build polygon from ways in order.
Overpass returns geometry arrays which are sequential segments.
We need to connect them properly.
"""
from shapely.geometry import LineString, Polygon
outer_ways = []
for m in rel.get('members', []):
if m['role'] == 'outer' and 'geometry' in m:
coords = [(pt['lon'], pt['lat']) for pt in m['geometry']]
if len(coords) >= 2:
outer_ways.append(coords)
if not outer_ways:
return None
# For Vienna districts, outer ways form closed rings
# Let's try to connect them by matching endpoints
# First, let's see if there's just one closed ring
if len(outer_ways) == 1 and len(outer_ways[0]) >= 3:
coords = outer_ways[0]
if coords[0] == coords[-1]:
return Polygon(coords)
else:
# Try to close it
return Polygon(coords + [coords[0]])
# Multiple ways - sort by connecting endpoints
# Build a dict mapping endpoint tuples to line segments
segments = []
for coords in outer_ways:
seg = LineString(coords)
segments.append(seg)
merged = unary_union(segments)
if merged.geom_type == 'LineString':
if merged.is_ring:
return Polygon(merged.coords)
else:
# Try to close
coords = list(merged.coords)
if coords[0] != coords[-1]:
coords.append(coords[0])
return Polygon(coords)
elif merged.geom_type == 'MultiLineString':
polygs = list(polygonize(merged.geoms))
if len(polygs) == 1:
return polygs[0]
elif len(polygs) > 1:
# Check for holes - inner rings
# For Vienna districts, there might be holes (enclaves)
# Sort by area, largest is outer
polygs_sorted = sorted(polygs, key=lambda p: p.area, reverse=True)
outer = polygs_sorted[0]
holes = [p.exterior.coords for p in polygs_sorted[1:] if p.area > 0.000001]
if holes:
return Polygon(outer.exterior.coords, holes)
return outer
else:
print(f" WARNING: cannot polygonize for rel {rel['id']}")
return None
else:
return None
def build_polygon_simple2(rel):
"""
Simpler approach: just collect all outer way coordinates,
close them, and build polygon via unary_union of LineStrings.
"""
from shapely.geometry import LineString
from shapely.ops import polygonize
lines = []
for m in rel.get('members', []):
if m['role'] == 'outer' and 'geometry' in m:
coords = [(pt['lon'], pt['lat']) for pt in m['geometry']]
if len(coords) >= 2:
lines.append(LineString(coords))
if not lines:
return None
merged = unary_union(lines)
if merged.geom_type == 'GeometryCollection':
geoms = [g for g in merged.geoms if g.geom_type in ('LineString', 'MultiLineString')]
if not geoms:
return None
merged = unary_union(geoms)
if merged.geom_type in ('LineString',):
if merged.is_ring:
return Polygon(merged)
else:
coords = list(merged.coords)
if len(coords) >= 3:
if coords[0] != coords[-1]:
coords.append(coords[0])
return Polygon(coords)
return None
elif merged.geom_type == 'MultiLineString':
polys = list(polygonize(merged.geoms))
if not polys:
return None
if len(polys) == 1:
return polys[0]
# Sort by area
polys.sort(key=lambda p: p.area, reverse=True)
outer_poly = polys[0]
holes = []
for p in polys[1:]:
if p.representative_point().within(outer_poly) and not p.touches(outer_poly):
holes.append(p.exterior.coords)
if holes:
return Polygon(outer_poly.exterior.coords, holes)
return outer_poly
else:
return None
def build_polygon_robust(rel):
"""
Build a polygon from the relation's outer way geometries.
This handles Vienna's district boundaries which are made of multiple ways.
"""
from shapely.geometry import LineString, Polygon, MultiLineString
from shapely.ops import polygonize, linemerge
# Collect all outer way geometries as LineStrings
lines = []
for m in rel.get('members', []):
if m['role'] == 'outer' and 'geometry' in m:
coords = [(pt['lon'], pt['lat']) for pt in m['geometry']]
if len(coords) >= 2:
lines.append(LineString(coords))
if not lines:
return None
# Merge connected lines
merged = linemerge(lines)
if merged.geom_type == 'LineString':
coords = list(merged.coords)
if coords[0] != coords[-1]:
coords.append(coords[0])
return Polygon(coords)
elif merged.geom_type == 'MultiLineString':
# Try to polygonize
polys = list(polygonize(merged.geoms))
if not polys:
return None
if len(polys) == 1:
return polys[0]
# Multiple polygons - check for holes
polys_sorted = sorted(polys, key=lambda p: p.area, reverse=True)
outer = polys_sorted[0]
holes = []
for p in polys_sorted[1:]:
# Check if this polygon is a hole (inner ring)
if p.area > 1e-10:
# Check if it's inside the outer polygon
centroid = p.representative_point()
if outer.contains(centroid) or outer.touches(p):
holes.append(p.exterior.coords)
if holes:
return Polygon(outer.exterior.coords, holes)
return outer
else:
return None
# Load data
print("Loading data...")
with open('/work/current_districts.json') as f:
current_data = json.load(f)
with open('/work/districts_2014.json') as f:
old_data = json.load(f)
# Build district polygons
print("Building current district polygons...")
current_polys = {}
for el in current_data['elements']:
if el['type'] == 'relation':
ref = el.get('tags', {}).get('ref')
name = el.get('tags', {}).get('name', '')
poly = build_polygon_robust(el)
if poly:
current_polys[ref] = {'name': name, 'polygon': poly, 'relation': el}
else:
print(f" FAILED to build polygon for current: {name} (ref={ref})")
print("Building 2014 district polygons...")
old_polys = {}
for el in old_data['elements']:
if el['type'] == 'relation':
ref = el.get('tags', {}).get('ref')
name = el.get('tags', {}).get('name', '')
poly = build_polygon_robust(el)
if poly:
old_polys[ref] = {'name': name, 'polygon': poly, 'relation': el}
else:
print(f" FAILED to build polygon for 2014: {name} (ref={ref})")
print(f"\nCurrent districts built: {len(current_polys)}")
print(f"2014 districts built: {len(old_polys)}")
# Check which refs match
print(f"\nCurrent refs: {sorted(current_polys.keys())}")
print(f"2014 refs: {sorted(old_polys.keys())}")
# Build complete Vienna boundaries for both time periods
print("\nComputing Vienna boundary differences...")
current_vienna = unary_union([current_polys[r]['polygon'] for r in current_polys])
old_vienna = unary_union([old_polys[r]['polygon'] for r in old_polys])
# Compute symmetric difference
# added_since_2014 = in current but not in 2014
added = current_vienna.difference(old_vienna)
# removed_since_2014 = in 2014 but not in current
removed = old_vienna.difference(current_vienna)
# unchanged = intersection
unchanged = current_vienna.intersection(old_vienna)
print(f" Added area: {added.area:.6f} (type={added.geom_type})")
print(f" Removed area: {removed.area:.6f} (type={removed.geom_type})")
print(f" Unchanged area: {unchanged.area:.6f}")
# Build features
features = []
def geom_to_feature(geom, change_type, district_name, touches_changed):
"""Convert a shapely geometry to a GeoJSON feature."""
if geom.is_empty:
return None
if geom.geom_type == 'GeometryCollection':
# Flatten
result = []
for g in geom.geoms:
f = geom_to_feature(g, change_type, district_name, touches_changed)
if f:
result.append(f)
return result
# Ensure MultiPolygon
if geom.geom_type == 'Polygon':
geom = MultiPolygon([geom])
elif geom.geom_type != 'MultiPolygon':
return None
return {
"type": "Feature",
"geometry": mapping(geom),
"properties": {
"change_type": change_type,
"district_name": district_name,
"touches_changed": touches_changed
}
}
# For added and removed, the district_name is not clearly one district
# We'll classify by which districts are involved
# For added: which current districts overlap with the added area
# For removed: which old districts overlap with the removed area
# Compute per-district changes
features_list = []
# Unchanged - per district
for ref in sorted(current_polys.keys(), key=int):
name = current_polys[ref]['name']
poly = current_polys[ref]['polygon']
district_unchanged = poly.intersection(unchanged)
district_added = poly.intersection(added)
district_removed = poly.intersection(removed)
touches = not district_added.is_empty or not district_removed.is_empty
if not district_unchanged.is_empty:
f = geom_to_feature(district_unchanged, 'unchanged', name, touches)
if f:
if isinstance(f, list):
features_list.extend(f)
else:
features_list.append(f)
if not district_added.is_empty:
f = geom_to_feature(district_added, 'added_since_2014', name, True)
if f:
if isinstance(f, list):
features_list.extend(f)
else:
features_list.append(f)
if not district_removed.is_empty:
f = geom_to_feature(district_removed, 'removed_since_2014', name, True)
if f:
if isinstance(f, list):
features_list.extend(f)
else:
features_list.append(f)
print(f"\nTotal features: {len(features_list)}")
# Build GeoJSON
geojson = {
"type": "FeatureCollection",
"features": features_list
}
with open('/work/vienna_boundary_changes.geojson', 'w') as f:
json.dump(geojson, f, indent=2)
print("\nSaved vienna_boundary_changes.geojson")
print(f"Feature count: {len(features_list)}")
# Summary stats
stats = {}
for feat in features_list:
ct = feat['properties']['change_type']
stats[ct] = stats.get(ct, 0) + 1
print(f"By type: {stats}")
Wrote /work/solve.py (12697 chars).
12:45
Let me also check if there are name differences that need normalizing. Let me run this first.
12:45
Bash
cd /work && timeout 120 python solve.py
Loading data...
Building current district polygons...
Building 2014 district polygons...
Current districts built: 23
2014 districts built: 23
Current refs: ['1', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '2', '20', '21', '22', '23', '3', '4', '5', '6', '7', '8', '9']
2014 refs: ['1', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '2', '20', '21', '22', '23', '3', '4', '5', '6', '7', '8', '9']
Computing Vienna boundary differences...
Added area: 0.000002 (type=MultiPolygon)
Removed area: 0.000002 (type=MultiPolygon)
Unchanged area: 0.050197
Total features: 33
Saved vienna_boundary_changes.geojson
Feature count: 33
By type: {'unchanged': 23, 'added_since_2014': 9, 'removed_since_2014': 1}
12:50
The changes are very small (0.000002 area in degrees). This could be due to very minor boundary changes, or it could be a geometry precision issue. Let me examine the results more carefully and also check the district name normalization. The names seem to already match between the two snapshots, but the task says "District names changed casing and spelling between snapshots, so normalise them to match." Let me check more carefully. Also, the added area found is very tiny - let me verify this is correct and not just numerical noise. Let me check the output and also improve the normalization.
12:50
Write
/work/check_result.py
content · 22 lines · py
import json
with open('/work/vienna_boundary_changes.geojson') as f:
data = json.load(f)
print(f"Total features: {len(data['features'])}")
# Check added features
for feat in data['features']:
props = feat['properties']
if props['change_type'] != 'unchanged':
geom = feat['geometry']
from shapely.geometry import shape
s = shape(geom)
print(f" change_type={props['change_type']} district={props['district_name']} touches={props['touches_changed']} area={s.area:.8f}")
print("\nAll district names in output:")
names = set()
for feat in data['features']:
names.add(feat['properties']['district_name'])
for n in sorted(names):
print(f" {n}")
Wrote /work/check_result.py (695 chars).
12:52
Bash
cd /work && python check_result.py
Total features: 33 change_type=added_since_2014 district=Favoriten touches=True area=0.00000038 change_type=added_since_2014 district=Simmering touches=True area=0.00000032 change_type=added_since_2014 district=Hietzing touches=True area=0.00000002 change_type=removed_since_2014 district=Hietzing touches=True area=0.00000000 change_type=added_since_2014 district=Penzing touches=True area=0.00000041 change_type=added_since_2014 district=Hernals touches=True area=0.00000003 change_type=added_since_2014 district=Döbling touches=True area=0.00000025 change_type=added_since_2014 district=Floridsdorf touches=True area=0.00000017 change_type=added_since_2014 district=Donaustadt touches=True area=0.00000012 change_type=added_since_2014 district=Liesing touches=True area=0.00000025 All district names in output: Alsergrund Brigittenau Donaustadt Döbling Favoriten Floridsdorf Hernals Hietzing Innere Stadt Josefstadt Landstraße Leopoldstadt Liesing Margareten Mariahilf Meidling Neubau Ottakring Penzing Rudolfsheim-Fünfhaus Simmering Wieden Währing
13:00
The results look reasonable - there are tiny differences in some district boundaries. However, I should check whether the very small areas (some as small as 0.00000000) are just numerical noise from coordinate precision differences, or actual boundary changes. Let me also verify the geometry for each district more carefully, and check if there are any areas that touch changed boundaries. Also, I need to check the "touches_changed" flag more carefully. For unchanged areas that are in districts that border any added/removed area, it should be True. Let me refine this. Also, I should verify that the polygon building is correct - maybe I should inspect a few polygons to make sure. Let me also check if there are really changes or if these are just floating point precision issues.
13:00
13:00
Write
/work/inspect_geom.py
content · 38 lines · py
import json
from shapely.geometry import shape, Polygon, MultiPolygon
from shapely.ops import unary_union
with open('/work/current_districts.json') as f:
cur = json.load(f)
# Check a polygon
for el in cur['elements']:
if el['tags'].get('ref') == '1': # Innere Stadt
print(f"Innere Stadt members:")
for m in el['members']:
if m['role'] == 'outer':
coords = [(pt['lon'], pt['lat']) for pt in m['geometry']]
print(f" way {m['ref']}: {len(coords)} pts, first={coords[0]}, last={coords[-1]}")
break
# Also check a district with changes, like Favoriten
for el in cur['elements']:
if el['tags'].get('ref') == '10': # Favoriten
print(f"\nFavoriten (current) members:")
for m in el['members']:
if m['role'] == 'outer':
coords = [(pt['lon'], pt['lat']) for pt in m['geometry']]
print(f" way {m['ref']}: {len(coords)} pts, first={coords[0]}, last={coords[-1]}")
break
# Check 2014 Favoriten
with open('/work/districts_2014.json') as f:
old = json.load(f)
for el in old['elements']:
if el['tags'].get('ref') == '10': # Favoriten
print(f"\nFavoriten (2014) members:")
for m in el['members']:
if m['role'] == 'outer':
coords = [(pt['lon'], pt['lat']) for pt in m['geometry']]
print(f" way {m['ref']}: {len(coords)} pts, first={coords[0]}, last={coords[-1]}")
break
Wrote /work/inspect_geom.py (1484 chars).
13:03
Bash
cd /work && python inspect_geom.py
Innere Stadt members: way 147416055: 61 pts, first=(16.375215, 48.1999528), last=(16.3848946, 48.2113547) way 147463458: 41 pts, first=(16.3848946, 48.2113547), last=(16.370149, 48.2184891) way 147396089: 33 pts, first=(16.370149, 48.2184891), last=(16.3565102, 48.2143408) way 147396086: 14 pts, first=(16.3565102, 48.2143408), last=(16.3558875, 48.2069081) way 147396090: 10 pts, first=(16.3558875, 48.2069081), last=(16.3615382, 48.2026474) way 147399477: 9 pts, first=(16.3615382, 48.2026474), last=(16.3659409, 48.1996821) way 147399466: 14 pts, first=(16.3659409, 48.1996821), last=(16.375215, 48.1999528) Favoriten (current) members: way 150887614: 30 pts, first=(16.3356406, 48.1592699), last=(16.375462, 48.1479138) way 147412715: 8 pts, first=(16.3356406, 48.1592699), last=(16.3319178, 48.1633398) way 350993577: 5 pts, first=(16.3319178, 48.1633398), last=(16.3372646, 48.1709121) way 352828698: 2 pts, first=(16.3372646, 48.1709121), last=(16.3373518, 48.1710406) way 147412714: 2 pts, first=(16.3373518, 48.1710406), last=(16.3374508, 48.1710281) way 33526676: 8 pts, first=(16.3374508, 48.1710281), last=(16.3383478, 48.1709207) way 357136448: 2 pts, first=(16.3383478, 48.1709207), last=(16.3391076, 48.170822) way 187831671: 2 pts, first=(16.3391076, 48.170822), last=(16.3394466, 48.1707781) way 135752427: 2 pts, first=(16.3394466, 48.1707781), last=(16.3397652, 48.1707386) way 187831673: 7 pts, first=(16.3397652, 48.1707386), last=(16.3424389, 48.1703933) way 357141690: 4 pts, first=(16.3424389, 48.1703933), last=(16.3431117, 48.1702951) way 351160311: 8 pts, first=(16.3431117, 48.1702951), last=(16.3445536, 48.1700237) way 355040417: 4 pts, first=(16.3451124, 48.1698965), last=(16.3445536, 48.1700237) way 355040416: 15 pts, first=(16.3474909, 48.1720558), last=(16.3451124, 48.1698965) way 355040415: 18 pts, first=(16.3474909, 48.1720558), last=(16.3460737, 48.1742019) way 211671729: 2 pts, first=(16.3460737, 48.1742019), last=(16.3454687, 48.1739951) way 149641272: 31 pts, first=(16.3454687, 48.1739951), last=(16.349725, 48.1792208) way 351251048: 9 pts, first=(16.3557569, 48.1797866), last=(16.349725, 48.1792208) way 351251057: 12 pts, first=(16.3596862, 48.1807316), last=(16.3557569, 48.1797866) way 150887613: 20 pts, first=(16.3688339, 48.1838352), last=(16.3596862, 48.1807316) way 351251050: 11 pts, first=(16.3735607, 48.1851384), last=(16.3688339, 48.1838352) way 150887611: 17 pts, first=(16.3809988, 48.1881182), last=(16.3735607, 48.1851384) way 351251058: 26 pts, first=(16.3847753, 48.1837312), last=(16.3809988, 48.1881182) way 149746699: 10 pts, first=(16.3954544, 48.1755271), last=(16.3847753, 48.1837312) way 390598534: 13 pts, first=(16.3970787, 48.1743676), last=(16.3954544, 48.1755271) way 1301555186: 2 pts, first=(16.3971845, 48.1743018), last=(16.3970787, 48.1743676) way 1301555187: 2 pts, first=(16.3971554, 48.1740636), last=(16.3971845, 48.1743018) way 149746696: 31 pts, first=(16.4362198, 48.1379787), last=(16.3971554, 48.1740636) way 59178611: 2 pts, first=(16.4362198, 48.1379787), last=(16.43313, 48.1378785) way 1391646370: 8 pts, first=(16.43313, 48.1378785), last=(16.433439, 48.1315569) way 355136961: 23 pts, first=(16.433439, 48.1315569), last=(16.4370202, 48.1199322) way 33118870: 14 pts, first=(16.4370202, 48.1199322), last=(16.4225504, 48.1225594) way 1391646369: 8 pts, first=(16.4225504, 48.1225594), last=(16.4128199, 48.1182387) way 33118671: 16 pts, first=(16.4128199, 48.1182387), last=(16.4028004, 48.1223583) way 351265908: 10 pts, first=(16.4028004, 48.1223583), last=(16.3910196, 48.1244957) way 1391646365: 4 pts, first=(16.3910196, 48.1244957), last=(16.3883438, 48.1253693) way 33118668: 6 pts, first=(16.3883438, 48.1253693), last=(16.3829747, 48.1263617) way 351265907: 17 pts, first=(16.3829747, 48.1263617), last=(16.3658913, 48.1283994) way 33118389: 3 pts, first=(16.3658913, 48.1283994), last=(16.3656377, 48.1286732) way 351265905: 34 pts, first=(16.374245, 48.1422088), last=(16.3656377, 48.1286732) way 855764994: 4 pts, first=(16.3770042, 48.1473033), last=(16.374245, 48.1422088) way 350993586: 4 pts, first=(16.3771178, 48.1475247), last=(16.3770042, 48.1473033) way 855764995: 3 pts, first=(16.375462, 48.1479138), last=(16.3771178, 48.1475247) Favoriten (2014) members: way 150887614: 46 pts, first=(16.3356483, 48.1592757), last=(16.3656351, 48.1286691) way 33118389: 3 pts, first=(16.3658883, 48.1284024), last=(16.3656351, 48.1286691) way 33118668: 18 pts, first=(16.3886968, 48.1252979), last=(16.3658883, 48.1284024) way 33118671: 24 pts, first=(16.4128199, 48.1182387), last=(16.3886968, 48.1252979) way 33118870: 15 pts, first=(16.437007, 48.1199415), last=(16.4128199, 48.1182387) way 59178611: 21 pts, first=(16.4362198, 48.1379787), last=(16.437007, 48.1199415) way 149746696: 35 pts, first=(16.4362198, 48.1379787), last=(16.3954544, 48.1755271) way 149746699: 24 pts, first=(16.3954544, 48.1755271), last=(16.3809988, 48.1881182) way 150887611: 22 pts, first=(16.3809988, 48.1881182), last=(16.3688339, 48.1838352) way 150887613: 30 pts, first=(16.3688339, 48.1838352), last=(16.3497539, 48.1792189) way 149641272: 12 pts, first=(16.3416033, 48.1764135), last=(16.3497539, 48.1792189) way 149641213: 2 pts, first=(16.3416033, 48.1764135), last=(16.3421118, 48.1752885) way 149641277: 2 pts, first=(16.3434088, 48.175364), last=(16.3421118, 48.1752885) way 149641280: 7 pts, first=(16.3454687, 48.1739951), last=(16.3434088, 48.175364) way 13836031: 12 pts, first=(16.347385, 48.1718515), last=(16.3460737, 48.1742019) way 23304527: 4 pts, first=(16.3455794, 48.1717043), last=(16.347385, 48.1718515) way 23304513: 10 pts, first=(16.3455794, 48.1717043), last=(16.3451514, 48.1698745) way 172969675: 2 pts, first=(16.3444712, 48.1700384), last=(16.3451514, 48.1698745) way 135752427: 2 pts, first=(16.3394466, 48.1707781), last=(16.3397652, 48.1707386) way 187831673: 6 pts, first=(16.3397652, 48.1707386), last=(16.3444712, 48.1700384) way 187831671: 2 pts, first=(16.3391076, 48.170822), last=(16.3394466, 48.1707781) way 33526676: 3 pts, first=(16.3374656, 48.1710308), last=(16.3391076, 48.170822) way 147412714: 2 pts, first=(16.3373691, 48.1710398), last=(16.3374656, 48.1710308) way 147412715: 12 pts, first=(16.3356483, 48.1592757), last=(16.3373691, 48.1710398) way 211671729: 2 pts, first=(16.3460737, 48.1742019), last=(16.3454687, 48.1739951)
13:28
I see the geometries look correct - the ways connect end-to-end. The changes seem very tiny (area ~0.000002), which could be from actual small boundary adjustments or from numerical precision differences. Let me also check the "touches_changed" flag more carefully - the task asks: "For each district flag whether its geometry borders any area that was added or removed." Currently I'm setting touches_changed per geometry fragment, but the question says per district. Let me re-read the requirements: "Classify each resulting geometry fragment by how it changed: added_since_2014, removed_since_2014, or unchanged. For each district flag whether its geometry borders any area that was added or removed." So: each output feature should have change_type, district_name, and touches_changed. The touches_changed is a boolean per district indicating if that district's geometry (any part) borders an area that was added or removed. Let me redo the solution: 1. Per district, find the changed geometries (added/removed/unchanged parts) 2. For each district, compute if any part of its boundary (current or 2014 version) is adjacent to changed areas 3. For each geometry fragment, set touches_changed to the district-level flag Let me also refine the polygon building to be more robust, and think about what "touches_changed" really means: - A district's geometry "borders any area that was added or removed" - so if the district (in either time period) touches added or removed areas. Let me redo this more carefully.
13:28
13:28
Write
/work/solve_final.py
content · 274 lines · py
import json
from shapely.geometry import Polygon, MultiPolygon, LineString, shape, mapping
from shapely.ops import unary_union, polygonize, linemerge
import geopandas as gpd
def build_polygon_robust(rel):
"""
Build a polygon from the relation's outer way geometries.
Vienna districts are made of multiple ways forming closed rings.
"""
lines = []
for m in rel.get('members', []):
if m['role'] == 'outer' and 'geometry' in m:
coords = [(pt['lon'], pt['lat']) for pt in m['geometry']]
if len(coords) >= 2:
lines.append(LineString(coords))
if not lines:
return None
merged = linemerge(lines)
if merged is None or merged.is_empty:
return None
if merged.geom_type == 'LineString':
coords = list(merged.coords)
if coords[0] != coords[-1]:
coords.append(coords[0])
return Polygon(coords)
elif merged.geom_type == 'MultiLineString':
polys = list(polygonize(merged.geoms))
if not polys:
return None
if len(polys) == 1:
return polys[0]
# Sort by area descending
polys_sorted = sorted(polys, key=lambda p: p.area, reverse=True)
outer = polys_sorted[0]
holes = []
for p in polys_sorted[1:]:
if p.area > 1e-10:
centroid = p.representative_point()
if outer.contains(centroid) or outer.touches(p):
holes.append(p.exterior.coords)
if holes:
return Polygon(outer.exterior.coords, holes)
return outer
else:
return None
def ensure_multipolygon(geom):
"""Convert to MultiPolygon if needed."""
if geom.is_empty:
return None
if geom.geom_type == 'Polygon':
return MultiPolygon([geom])
if geom.geom_type == 'MultiPolygon':
return geom
if geom.geom_type == 'GeometryCollection':
polys = [g for g in geom.geoms if g.geom_type in ('Polygon', 'MultiPolygon')]
if not polys:
return None
merged = unary_union(polys)
return ensure_multipolygon(merged)
return None
# Load data
print("Loading data...")
with open('/work/current_districts.json') as f:
current_data = json.load(f)
with open('/work/districts_2014.json') as f:
old_data = json.load(f)
# Build polygons for each district, keyed by ref number
print("Building district polygons...")
current_districts = {} # ref -> {name, polygon}
old_districts = {}
for el in current_data['elements']:
if el['type'] == 'relation':
ref = el.get('tags', {}).get('ref')
name = el.get('tags', {}).get('name', '')
poly = build_polygon_robust(el)
if poly and not poly.is_empty:
current_districts[ref] = {'name': name, 'polygon': poly}
else:
print(f" FAILED: current {name} (ref={ref})")
for el in old_data['elements']:
if el['type'] == 'relation':
ref = el.get('tags', {}).get('ref')
name = el.get('tags', {}).get('name', '')
poly = build_polygon_robust(el)
if poly and not poly.is_empty:
old_districts[ref] = {'name': name, 'polygon': poly}
else:
print(f" FAILED: 2014 {name} (ref={ref})")
print(f"Current districts: {len(current_districts)}")
print(f"2014 districts: {len(old_districts)}")
# Build complete Vienna outlines
current_vienna = unary_union([current_districts[r]['polygon'] for r in current_districts])
old_vienna = unary_union([old_districts[r]['polygon'] for r in old_districts])
# Buffer slightly to handle numerical precision issues
BUF = 1e-10
# Compute the symmetric difference areas
added_total = current_vienna.difference(old_vienna)
removed_total = old_vienna.difference(current_vienna)
unchanged_total = current_vienna.intersection(old_vienna)
# Clean up small slivers
def clean_geom(geom, min_area=1e-10):
if geom.is_empty:
return None
if geom.geom_type == 'MultiPolygon':
polys = [p for p in geom.geoms if p.area >= min_area]
if not polys:
return None
if len(polys) == 1:
g = polys[0]
else:
g = MultiPolygon(polys)
else:
g = geom
return ensure_multipolygon(g)
added_total = clean_geom(added_total)
removed_total = clean_geom(removed_total)
print(f"\nAdded area: {added_total.area:.10f}" if added_total else "Added: None")
print(f"Removed area: {removed_total.area:.10f}" if removed_total else "Removed: None")
print(f"Unchanged area: {unchanged_total.area:.10f}")
# Compute per-district properties
# For each district (by ref), get its current and 2014 polygon
# Determine which parts are added, removed, unchanged
features = []
# Check all district refs (union of both)
all_refs = sorted(set(list(current_districts.keys()) + list(old_districts.keys())), key=int)
for ref in all_refs:
cur_info = current_districts.get(ref)
old_info = old_districts.get(ref)
# Use current name if available, else 2014 name
if cur_info:
name = cur_info['name']
cur_poly = cur_info['polygon']
else:
name = old_info['name']
cur_poly = None
if old_info:
old_poly = old_info['polygon']
else:
old_poly = None
# Compute district-level changes
district_added = None
district_removed = None
district_unchanged = None
if cur_poly:
# What's in this district's current area
if added_total and not added_total.is_empty:
district_added = cur_poly.intersection(added_total)
district_added = clean_geom(district_added)
district_unchanged = cur_poly.intersection(unchanged_total)
district_unchanged = clean_geom(district_unchanged)
if old_poly:
if removed_total and not removed_total.is_empty:
district_removed = old_poly.intersection(removed_total)
district_removed = clean_geom(district_removed)
# Does this district border any changed area?
# Check if the district polygon (either current or old) touches added or removed areas
touches_changed = False
if cur_poly:
if added_total and not added_total.is_empty:
if cur_poly.touches(added_total) or cur_poly.intersects(added_total.buffer(BUF)):
touches_changed = True
if removed_total and not removed_total.is_empty:
if cur_poly.touches(removed_total) or cur_poly.intersects(removed_total.buffer(BUF)):
touches_changed = True
if old_poly and not touches_changed:
if added_total and not added_total.is_empty:
if old_poly.touches(added_total) or old_poly.intersects(added_total.buffer(BUF)):
touches_changed = True
if removed_total and not removed_total.is_empty:
if old_poly.touches(removed_total) or old_poly.intersects(removed_total.buffer(BUF)):
touches_changed = True
# Create features
if district_unchanged:
f = {
"type": "Feature",
"geometry": mapping(district_unchanged),
"properties": {
"change_type": "unchanged",
"district_name": name,
"touches_changed": touches_changed
}
}
features.append(f)
if district_added:
f = {
"type": "Feature",
"geometry": mapping(district_added),
"properties": {
"change_type": "added_since_2014",
"district_name": name,
"touches_changed": True
}
}
features.append(f)
if district_removed:
f = {
"type": "Feature",
"geometry": mapping(district_removed),
"properties": {
"change_type": "removed_since_2014",
"district_name": name,
"touches_changed": True
}
}
features.append(f)
print(f"\nTotal features: {len(features)}")
# Build final GeoJSON
geojson = {
"type": "FeatureCollection",
"features": features
}
with open('/work/vienna_boundary_changes.geojson', 'w') as f:
json.dump(geojson, f, indent=2)
print("Saved vienna_boundary_changes.geojson")
# Summary
stats = {}
for feat in features:
ct = feat['properties']['change_type']
stats[ct] = stats.get(ct, 0) + 1
print(f"By type: {stats}")
# Check touches_changed values
touches_stats = {}
for feat in features:
tc = feat['properties']['touches_changed']
touches_stats[tc] = touches_stats.get(tc, 0) + 1
print(f"By touches_changed: {touches_stats}")
# Print details
for feat in features:
p = feat['properties']
geom = shape(feat['geometry'])
print(f" {p['change_type']:20s} | {p['district_name']:25s} | touches={str(p['touches_changed']):5s} | area={geom.area:.10f}")
Wrote /work/solve_final.py (8962 chars).
13:30
Bash
cd /work && timeout 120 python solve_final.py
Loading data...
Building district polygons...
Current districts: 23
2014 districts: 23
Added area: 0.0000019526
Removed area: 0.0000016865
Unchanged area: 0.0501965578
Total features: 41
Saved vienna_boundary_changes.geojson
By type: {'unchanged': 23, 'added_since_2014': 9, 'removed_since_2014': 9}
By touches_changed: {False: 14, True: 27}
unchanged | Innere Stadt | touches=False | area=0.0003470665
unchanged | Leopoldstadt | touches=False | area=0.0023271617
unchanged | Landstraße | touches=False | area=0.0008959061
unchanged | Wieden | touches=False | area=0.0002148371
unchanged | Margareten | touches=False | area=0.0002432927
unchanged | Mariahilf | touches=False | area=0.0001761840
unchanged | Neubau | touches=False | area=0.0001945226
unchanged | Josefstadt | touches=False | area=0.0001318855
unchanged | Alsergrund | touches=False | area=0.0003591304
unchanged | Favoriten | touches=True | area=0.0038459194
added_since_2014 | Favoriten | touches=True | area=0.0000003801
removed_since_2014 | Favoriten | touches=True | area=0.0000002457
unchanged | Simmering | touches=True | area=0.0028113669
added_since_2014 | Simmering | touches=True | area=0.0000003223
removed_since_2014 | Simmering | touches=True | area=0.0000000617
unchanged | Meidling | touches=False | area=0.0009796713
unchanged | Hietzing | touches=True | area=0.0045602415
added_since_2014 | Hietzing | touches=True | area=0.0000000236
removed_since_2014 | Hietzing | touches=True | area=0.0000000354
unchanged | Penzing | touches=True | area=0.0040855011
added_since_2014 | Penzing | touches=True | area=0.0000004106
removed_since_2014 | Penzing | touches=True | area=0.0000002790
unchanged | Rudolfsheim-Fünfhaus | touches=False | area=0.0004738972
unchanged | Ottakring | touches=False | area=0.0010494819
unchanged | Hernals | touches=True | area=0.0013795330
added_since_2014 | Hernals | touches=True | area=0.0000000304
removed_since_2014 | Hernals | touches=True | area=0.0000000584
unchanged | Währing | touches=False | area=0.0007683824
unchanged | Döbling | touches=True | area=0.0030210545
added_since_2014 | Döbling | touches=True | area=0.0000002451
removed_since_2014 | Döbling | touches=True | area=0.0000000765
unchanged | Brigittenau | touches=False | area=0.0006916144
unchanged | Floridsdorf | touches=True | area=0.0053845325
added_since_2014 | Floridsdorf | touches=True | area=0.0000001654
removed_since_2014 | Floridsdorf | touches=True | area=0.0000001111
unchanged | Donaustadt | touches=True | area=0.0123806994
added_since_2014 | Donaustadt | touches=True | area=0.0000001223
removed_since_2014 | Donaustadt | touches=True | area=0.0000003333
unchanged | Liesing | touches=True | area=0.0038746758
added_since_2014 | Liesing | touches=True | area=0.0000002527
removed_since_2014 | Liesing | touches=True | area=0.0000004854
14:01
The results look reasonable - there are small changes along district boundaries between 2014 and now, which is expected due to mapping improvements and real boundary changes. The areas are tiny (in decimal degree units), which is normal for coordinate differences. Let me verify the output and also make sure the `touches_changed` is correctly set for districts that border added/removed areas. Let me check if some districts that touch changes might be missed - like, any district that has `added_since_2014` or `removed_since_2014` should have `touches_changed=True` (already done), and any unchanged district that touches a changed area should also have `touches_changed=True`. Looking at the output: - Favoriten has added and removed, touches_changed=True ✓ - Simmering has added and removed, touches_changed=True ✓ - Hietzing has added and removed, touches_changed=True ✓ - Penzing has added and removed, touches_changed=True ✓ - Hernals has added and removed, touches_changed=True ✓ - Döbling has added and removed, touches_changed=True ✓ - Floridsdorf has added and removed, touches_changed=True ✓ - Donaustadt has added and removed, touches_changed=True ✓ - Liesing has added and removed, touches_changed=True ✓ And the unchanged districts that touch changes: - Favoriten unchanged → touches=True ✓ - Simmering unchanged → touches=True ✓ - Hietzing unchanged → touches=True ✓ - Penzing unchanged → touches=True ✓ - Hernals unchanged → touches=True ✓ - Döbling unchanged → touches=True ✓ - Floridsdorf unchanged → touches=True ✓ - Donaustadt unchanged → touches=True ✓ - Liesing unchanged → touches=True ✓ That's correct because those districts have the added/removed parts. But what about districts that DON'T have added/removed parts but border a district that does? For example, if Innere Stadt borders a district with changes... Actually, the added/removed parts are on the outer boundary of Vienna or between districts. Let me think about which districts DON'T have added/removed parts but might be adjacent to ones that do. Actually, the way I've computed `touches_changed` - I'm checking if the district polygon touches the total added/removed areas. If an unchanged district shares a border with a changed area (e.g., the changed area is on the boundary between two districts), both districts would touch it. But since I'm only using `added_total` and `removed_total` (the sum of all changes), and they're typically small slivers along boundaries, a district might not actually touch the change if the change is entirely inside another district. Let me re-examine: the added and removed areas come from `current_vienna.difference(old_vienna)` and vice versa. These represent the symmetric difference of the whole city outlines, so they could be along the outer boundary OR along internal administrative boundaries that changed. Actually, since we're comparing Vienna's total boundary (union of all districts), any change in internal boundaries BETWEEN districts would not show up in the symmetric difference of the union! The union of all districts is the same regardless of how the internal boundaries are drawn. Let me think more carefully: - `current_vienna = union of all current district polygons` - `old_vienna = union of all old district polygons` - `added = current_vienna - old_vienna` → areas that are in current Vienna but not in old Vienna - `removed = old_vienna - current_vienna` → areas that are in old Vienna but not in current Vienna These only capture changes in the OUTER boundary of Vienna (expansion/contraction of the city limits), not internal administrative boundary changes. For internal boundary changes, I need to compare each district with its 2014 counterpart individually. Let me reconsider the approach. The task says "which parts of the district boundaries have changed between then and now" and to classify fragments as added_since_2014, removed_since_2014, or unchanged. This should capture ALL boundary changes, including when a boundary between two districts shifts. So I think the correct approach is: 1. For each district, compare its current polygon with its 2014 polygon 2. The symmetric difference for each district pair gives: - added: area in current but not in 2014 (this district gained) - removed: area in 2014 but not in current (this district lost) - unchanged: area present in both Then the `touches_changed` flag would indicate whether any part of the district's boundary (current or old) touches any changed area. Let me redo this properly.
14:01
14:01
Write
/work/solve_v2.py
content · 240 lines · py
import json
from shapely.geometry import Polygon, MultiPolygon, LineString, shape, mapping
from shapely.ops import unary_union, polygonize, linemerge
def build_polygon(rel):
"""Build a polygon from the relation's outer way geometries."""
lines = []
for m in rel.get('members', []):
if m['role'] == 'outer' and 'geometry' in m:
coords = [(pt['lon'], pt['lat']) for pt in m['geometry']]
if len(coords) >= 2:
lines.append(LineString(coords))
if not lines:
return None
merged = linemerge(lines)
if merged is None or merged.is_empty:
return None
if merged.geom_type == 'LineString':
coords = list(merged.coords)
if coords[0] != coords[-1]:
coords.append(coords[0])
return Polygon(coords)
elif merged.geom_type == 'MultiLineString':
polys = list(polygonize(merged.geoms))
if not polys:
return None
if len(polys) == 1:
return polys[0]
polys_sorted = sorted(polys, key=lambda p: p.area, reverse=True)
outer = polys_sorted[0]
holes = []
for p in polys_sorted[1:]:
if p.area > 1e-10:
centroid = p.representative_point()
if outer.contains(centroid) or outer.touches(p):
holes.append(p.exterior.coords)
if holes:
return Polygon(outer.exterior.coords, holes)
return outer
return None
def to_multipolygon(geom):
"""Convert to MultiPolygon, return None if empty."""
if geom is None or geom.is_empty:
return None
if geom.geom_type == 'Polygon':
return MultiPolygon([geom])
if geom.geom_type == 'MultiPolygon':
return geom
if geom.geom_type == 'GeometryCollection':
polys = [g for g in geom.geoms if g.geom_type in ('Polygon', 'MultiPolygon')]
if not polys:
return None
merged = unary_union(polys)
return to_multipolygon(merged)
return None
def clean_slivers(geom, min_area=1e-10):
"""Remove tiny sliver polygons."""
if geom is None:
return None
geom = to_multipolygon(geom)
if geom is None:
return None
polys = [p for p in geom.geoms if p.area >= min_area]
if not polys:
return None
if len(polys) == 1:
return MultiPolygon([polys[0]])
return MultiPolygon(polys)
# Load data
print("Loading data...")
with open('/work/current_districts.json') as f:
current_data = json.load(f)
with open('/work/districts_2014.json') as f:
old_data = json.load(f)
print("Building district polygons...")
current_districts = {}
old_districts = {}
for el in current_data['elements']:
if el['type'] == 'relation':
ref = el.get('tags', {}).get('ref')
name = el.get('tags', {}).get('name', '')
poly = build_polygon(el)
if poly:
current_districts[ref] = {'name': name, 'polygon': poly}
for el in old_data['elements']:
if el['type'] == 'relation':
ref = el.get('tags', {}).get('ref')
name = el.get('tags', {}).get('name', '')
poly = build_polygon(el)
if poly:
old_districts[ref] = {'name': name, 'polygon': poly}
print(f"Current: {len(current_districts)}, 2014: {len(old_districts)}")
# Collect all refs
all_refs = sorted(set(list(current_districts.keys()) + list(old_districts.keys())), key=int)
# For each district, compute the symmetric difference between current and 2014
features = []
BUF = 1e-10
# Also compute overall changes for boundary detection
# Collect all changed areas (union of all per-district changes)
all_changed_areas = []
for ref in all_refs:
cur_info = current_districts.get(ref)
old_info = old_districts.get(ref)
if cur_info:
cur_name = cur_info['name']
cur_poly = cur_info['polygon']
else:
cur_name = old_info['name']
cur_poly = None
if old_info:
old_poly = old_info['polygon']
else:
old_poly = None
if cur_poly and old_poly:
# Normalise names - use the normalised name (current name)
name = cur_info['name']
# Compute differences
added = cur_poly.difference(old_poly)
removed = old_poly.difference(cur_poly)
unchanged = cur_poly.intersection(old_poly)
added = clean_slivers(added)
removed = clean_slivers(removed)
unchanged = clean_slivers(unchanged)
if added:
all_changed_areas.append(added)
if removed:
all_changed_areas.append(removed)
touches = False # Will compute later
if added:
features.append({
"type": "Feature",
"geometry": mapping(added),
"properties": {"change_type": "added_since_2014", "district_name": name, "touches_changed": False}
})
touches = True
if removed:
features.append({
"type": "Feature",
"geometry": mapping(removed),
"properties": {"change_type": "removed_since_2014", "district_name": name, "touches_changed": False}
})
touches = True
if unchanged:
features.append({
"type": "Feature",
"geometry": mapping(unchanged),
"properties": {"change_type": "unchanged", "district_name": name, "touches_changed": False}
})
elif cur_poly and not old_poly:
# New district (existed in current but not 2014 - unlikely for Vienna)
name = cur_info['name']
features.append({
"type": "Feature",
"geometry": mapping(to_multipolygon(cur_poly)),
"properties": {"change_type": "added_since_2014", "district_name": name, "touches_changed": False}
})
all_changed_areas.append(to_multipolygon(cur_poly))
elif old_poly and not cur_poly:
# District removed (existed in 2014 but not current)
name = old_info['name']
features.append({
"type": "Feature",
"geometry": mapping(to_multipolygon(old_poly)),
"properties": {"change_type": "removed_since_2014", "district_name": name, "touches_changed": False}
})
all_changed_areas.append(to_multipolygon(old_poly))
print(f"Features before touches: {len(features)}")
# Compute the union of all changed areas
if all_changed_areas:
changed_union = unary_union(all_changed_areas)
changed_union = clean_slivers(changed_union)
else:
changed_union = None
print(f"Changed union: {changed_union.area if changed_union else 0}")
# Now set touches_changed for each feature
for feat in features:
p = feat['properties']
geom = shape(feat['geometry'])
if p['change_type'] in ('added_since_2014', 'removed_since_2014'):
p['touches_changed'] = True
else:
# unchanged - check if this geometry touches any changed area
if changed_union and not changed_union.is_empty:
# Buffer slightly for robustness
touches = geom.buffer(BUF).intersects(changed_union.buffer(BUF))
p['touches_changed'] = bool(touches)
else:
p['touches_changed'] = False
# Build GeoJSON
geojson = {
"type": "FeatureCollection",
"features": features
}
with open('/work/vienna_boundary_changes.geojson', 'w') as f:
json.dump(geojson, f, indent=2)
print("Saved vienna_boundary_changes.geojson")
# Summary
stats = {}
for feat in features:
ct = feat['properties']['change_type']
stats[ct] = stats.get(ct, 0) + 1
print(f"By type: {stats}")
touches_stats = {}
for feat in features:
tc = feat['properties']['touches_changed']
touches_stats[tc] = touches_stats.get(tc, 0) + 1
print(f"By touches_changed: {touches_stats}")
for feat in features:
p = feat['properties']
g = shape(feat['geometry'])
print(f" {p['change_type']:20s} | {p['district_name']:25s} | touches={str(p['touches_changed']):5s} | area={g.area:.12f}")
Wrote /work/solve_v2.py (8098 chars).