OpenStreetMap changes in an area between two dates
Which restaurants opened, closed or changed in a part of a city between two dates, from the OpenStreetMap history, with one Overpass query and a short Python script.
[adiff:"START","END"]. Each <action> in the answer is a change: create for a new element, delete with visible="false" for a deleted one, delete with visible="true" for one that is still on the map but no longer matches, and modify for an edit. A version that does not match the query comes without tags. Ask for the elements of the third kind at the end date, by id, to see what they became.
The script
The restaurants of the Niederdorf, the old town of Zurich east of the Limmat, from 1 January 2023 to 1 January 2025. It needs Python 3 and nothing else; the key comes from the environment variable OVERPASS_CH_KEY.
import json
import os
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
URL = "https://api.overpass.ch/v1/" + os.environ["OVERPASS_CH_KEY"] + "/interpreter"
BBOX = "47.369,8.541,47.376,8.547" # the Niederdorf in Zurich
START, END = "2023-01-01T00:00:00Z", "2025-01-01T00:00:00Z"
def overpass(query):
data = urllib.parse.urlencode({"data": query}).encode()
return urllib.request.urlopen(URL, data=data, timeout=90).read()
def name(el):
t = el.find("tag[@k='name']")
return t.get("v") if t is not None else f"{el.tag} {el.get('id')}"
# 1. every change to the restaurants of the area between the two dates
root = ET.fromstring(overpass(f"""[out:xml][timeout:60][adiff:"{START}","{END}"];
nw({BBOX})(if:t["amenity"]=="restaurant");
out meta center;"""))
gone = {} # still on the map, but no longer a restaurant
edited = 0
for action in root.findall("action"):
kind = action.get("type")
old, new = action.find("old/*"), action.find("new/*")
# a version that does not match the query comes without tags
if kind == "create" or (kind == "modify" and not old.findall("tag")):
el = new if new is not None else action[0]
print(f"opened {name(el)} ({el.get('timestamp')[:10]}, {el.get('user')})")
elif kind == "delete" and new.get("visible") == "false":
print(f"deleted {name(old)} ({new.get('timestamp')[:10]}, {new.get('user')})")
elif kind == "delete":
gone[(old.tag, int(old.get("id")))] = name(old)
else:
edited += 1
# 2. what the others are at the end date
if gone:
ids = "".join(f"{t}({i});" for t, i in gone)
for e in json.loads(overpass(f'[out:json][date:"{END}"];({ids});out tags;'))["elements"]:
tags = e.get("tags", {})
now = next((f"{k}={v}" for k, v in tags.items() if k in ("amenity", "shop") or k.startswith("disused:")), "no tags")
print(f"changed {gone[(e['type'], e['id'])]}: now {now}")
print(f"{edited} other restaurants were edited (opening hours, website, ...)")
The result
opened Da Raffaele (2024-09-27, 1ucian0) opened Rank (2023-05-27, Fabilando) opened Rank (2024-10-26, paulbrunner) opened Wien Zeile (2024-07-23, 1ucian0) deleted Kormasutra (2023-04-24, paulbrunner) opened Rising Sun (2023-04-24, paulbrunner) deleted Bauernschänke (2023-04-22, paulbrunner) deleted Long Wai (2023-04-17, d_berger) deleted node 9270127717 (2023-04-17, d_berger) deleted node 9870177417 (2023-11-02, baprischka) opened Hirschli (2023-09-02, 1ucian0) opened roots (2023-09-02, 1ucian0) opened Restaurant zum weissen Kreuz (2024-04-01, d_berger) changed Enzian vegan food: now amenity=fast_food changed Isebähnli: now amenity=bar changed The Butcher: now disused:amenity=restaurant changed Öpfelchammer: now disused:amenity=restaurant changed barfüsser: now no tags 61 other restaurants were edited (opening hours, website, ...)
Both requests together took less than 3 seconds. A query on the same area with [out:json] at each of the two dates gives the same totals: 8 restaurants more, 10 fewer, 61 edited.
Reading it
- An opening is an element that matches the query at the end date and not at the start: a new point, or an existing one that became a restaurant. In the answer the second kind is a
modifywhose old version has no tags. - A deletion is not always a closure. Bauernschänke was a second point for the restaurant Bauern Schänke; the mapper who edited the first point on 22 April 2023 deleted it on the same day. Look for the same name nearby before you count a closure.
- An element that no longer matches is still on the map. Here Enzian vegan food is a Burger King now, Isebähnli a bar, and The Butcher and Öpfelchammer are tagged
disused:amenity=restaurant, the tag mappers use for a place that has closed. out meta centeradds the time and the user of each version and a centre point for buildings, so you can put the changes on a map.
Other places and other tags
- Another area: change
BBOX(south, west, north, east), or usearea["name"="…"]->.a; nw(area.a). Area filters use the boundary of today, also at a past date. - Shops instead of restaurants:
(if:t["shop"]!=""). Filters withif:are much faster at a past date than filters in square brackets. - The changes of the past week, every week: use a saved query on the current data instead. Saved queries run on a schedule and report what was added, removed and changed since the last run.
On overpass.ch
History queries are part of the Medium and Large plans; other plans get HTTP 403 history_not_in_plan. The history starts on 12 September 2012. Overpass history queries explains [date:], [diff:] and [adiff:], and the example with the same restaurants shows them on a map.