1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
#!/usr/bin/env python3
"""周辺公園・周辺施設の city が空のとき、Nominatimで市区町村を補完する。
使い方:
py -X utf8 scripts/fill-nearby-cities.py
- src/data/kodaira-nearby-parks.json の properties.city
- src/data/kodaira-nearby-facilities.json の city
既に city が入っている行は変更しない。Nominatimの利用ポリシーに従い、
逆ジオコーディングは1リクエストあたり約1.1秒待つ。
"""
import json
import os
import re
import time
import urllib.parse
import urllib.request
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PARKS = os.path.join(ROOT, "src", "data", "kodaira-nearby-parks.json")
FACILITIES = os.path.join(ROOT, "src", "data", "kodaira-nearby-facilities.json")
UA = "yasutakeyohei.com nearby city fill (https://yasutakeyohei.com/)"
def centroid(feature):
geom = feature.get("geometry") or {}
if geom.get("type") == "Point":
lon, lat = geom["coordinates"][:2]
return lat, lon
if geom.get("type") == "Polygon":
ring = geom.get("coordinates", [[]])[0]
elif geom.get("type") == "MultiPolygon":
ring = geom.get("coordinates", [[[]]])[0][0]
else:
return None, None
if not ring:
return None, None
lon = sum(p[0] for p in ring) / len(ring)
lat = sum(p[1] for p in ring) / len(ring)
return lat, lon
def city_from_text(text):
text = text or ""
# 「東村山市」のように市区町村名の中に「村」が含まれるため、市→町→村の順で探す。
for suffix in ("市", "町", "村"):
match = re.search(r"(?:東京都)?([^\s0-9]+?" + suffix + r")", text)
if match:
return match.group(1)
return ""
def reverse_city(lat, lon):
params = urllib.parse.urlencode(
{
"format": "jsonv2",
"lat": lat,
"lon": lon,
"zoom": 10,
"addressdetails": 1,
"accept-language": "ja",
}
)
url = "https://nominatim.openstreetmap.org/reverse?" + params
req = urllib.request.Request(url, headers={"User-Agent": UA})
try:
with urllib.request.urlopen(req, timeout=30) as res:
data = json.load(res)
except Exception as exc: # noqa: BLE001
print(f" warning: reverse failed ({lat},{lon}): {exc}")
return ""
finally:
time.sleep(1.1)
address = data.get("address") or {}
for key in ("city", "town", "village", "municipality"):
value = (address.get(key) or "").strip()
if value:
return value
return ""
def main():
parks_data = json.load(open(PARKS, encoding="utf-8"))
parks_changed = 0
for feature in parks_data.get("features", []):
props = feature.setdefault("properties", {})
if (props.get("city") or "").strip():
continue
lat, lon = centroid(feature)
if lat is None or lon is None:
continue
city = reverse_city(lat, lon)
if city:
props["city"] = city
parks_changed += 1
print(f" park: {props.get('name')} -> {city}")
if parks_changed:
with open(PARKS, "w", encoding="utf-8") as fh:
json.dump(parks_data, fh, ensure_ascii=False, separators=(",", ":"))
fh.write("\n")
facilities = json.load(open(FACILITIES, encoding="utf-8"))
facilities_changed = 0
for item in facilities:
if (item.get("city") or "").strip():
continue
city = city_from_text(item.get("address", "")) or city_from_text(item.get("name", ""))
if not city:
lat = item.get("lat")
lon = item.get("lon")
if lat is not None and lon is not None:
city = reverse_city(lat, lon)
if city:
item["city"] = city
facilities_changed += 1
print(f" facility: {item.get('name')} -> {city}")
if facilities_changed:
with open(FACILITIES, "w", encoding="utf-8") as fh:
json.dump(facilities, fh, ensure_ascii=False, indent=2)
fh.write("\n")
print(f"updated parks {parks_changed}, facilities {facilities_changed}")
if __name__ == "__main__":
main()
|