Files
cpc-direct-stations/fuel_coords_db.py
Timmy 876b7483ba Add offline OSM fuel station database
- Add fuel_coords_db.py to download OSM fuel station data
- 1299 CPC/related stations with coordinates
- Query order: cache -> OSM database -> Nominatim API
- Build database with: python3 fuel_coords_db.py --download

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 09:42:03 +08:00

190 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""建立台灣加油站座標資料庫來源OpenStreetMap Overpass API
用法:
# 下載 OSM 加油站資料並建立資料庫
python3 fuel_coords_db.py --download
# 查詢座標
python3 fuel_coords_db.py --query "台灣中油 社子"
"""
from __future__ import annotations
import argparse
import json
import math
import os
import urllib.request
from pathlib import Path
from typing import Dict, List, Optional
DB_FILE = Path(__file__).parent / "fuel_stations.json"
OVERPASS_URL = "https://overpass-api.de/api/interpreter"
def download_osm_fuel_stations() -> List[Dict]:
"""從 OpenStreetMap Overpass API 下載台灣加油站資料。"""
query = """
[out:json][timeout:300];
area["name:zh-Hant"="臺灣"]->.searchArea;
(
node["amenity"="fuel"]["operator"~"CPC|中油|台灣中油"](area.searchArea);
way["amenity"="fuel"]["operator"~"CPC|中油|台灣中油"](area.searchArea);
relation["amenity"="fuel"]["operator"~"CPC|中油|台灣中油"](area.searchArea);
node["amenity"="fuel"]["brand"~"CPC|中油|台灣中油"](area.searchArea);
way["amenity"="fuel"]["brand"~"CPC|中油|台灣中油"](area.searchArea);
relation["amenity"="fuel"]["brand"~"CPC|中油|台灣中油"](area.searchArea);
node["amenity"="fuel"]["name"~"台塑|台亞"](area.searchArea);
way["amenity"="fuel"]["name"~"台塑|台亞"](area.searchArea);
);
out center;
"""
print("下載 OSM 加油站資料...", flush=True)
req = urllib.request.Request(
OVERPASS_URL,
data=query.encode("utf-8"),
headers={"Content-Type": "text/plain"},
)
with urllib.request.urlopen(req, timeout=300) as response:
data = json.loads(response.read().decode("utf-8"))
print(f"找到 {len(data.get('elements', []))} 個加油站", flush=True)
return data.get("elements", [])
def build_database(elements: List[Dict]) -> Dict[str, Dict]:
"""建立站名 -> 座標 的對應資料庫。"""
db = {}
addresses = {} # 地址 -> 座標 (用於地址匹配)
for elem in elements:
tags = elem.get("tags", {})
name = tags.get("name") or tags.get("name:zh") or tags.get("name:zh-Hant")
if not name:
name = "加油站"
# 取得座標
if elem["type"] == "node":
lat, lon = elem["lat"], elem["lon"]
else:
lat, lon = elem.get("center", {}).get("lat"), elem.get("center", {}).get("lon")
if lat is None or lon is None:
continue
city = tags.get("addr:city") or tags.get("addr:city:zh") or ""
street = tags.get("addr:street") or tags.get("addr:street:zh") or ""
number = tags.get("addr:housenumber") or ""
# 建立多種查詢鍵
keys = [name]
if city:
keys.extend([f"{city}{name}", f"{name} ({city})"])
brand = tags.get("brand") or tags.get("operator")
if brand and brand not in name:
keys.append(f"{brand} {name}")
# 如果有完整地址,也加入地址鍵
if street:
full_addr = f"{city}{street}{number}".strip()
if full_addr:
addresses[full_addr] = {"lat": lat, "lon": lon, "source": "osm"}
keys.append(full_addr)
for key in keys:
key = key.strip()
if key and key not in db:
db[key] = {"lat": lat, "lon": lon, "source": "osm"}
# 合併地址庫
db.update(addresses)
return db
def save_database(db: Dict) -> None:
"""儲存資料庫。"""
with open(DB_FILE, "w", encoding="utf-8") as f:
json.dump(db, f, ensure_ascii=False, indent=2)
print(f"資料庫已儲存至 {DB_FILE}")
def load_database() -> Dict:
"""載入資料庫。"""
if DB_FILE.exists():
with open(DB_FILE, "r", encoding="utf-8") as f:
return json.load(f)
return {}
def haversine_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
"""計算兩點間的距離(公里)。"""
R = 6371 # 地球半徑(公里)
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = (math.sin(dlat / 2) ** 2 +
math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) *
math.sin(dlon / 2) ** 2)
c = 2 * math.asin(math.sqrt(a))
return R * c
def query_coords(name: str, city: str = "", db: Dict | None = None) -> Optional[Dict[str, float]]:
"""查詢加油站座標。"""
if db is None:
db = load_database()
if not db:
return None
# 精確匹配
queries = [name]
if city:
queries.extend([f"{city}{name}", f"{name} ({city})"])
for q in queries:
if q in db:
return db[q]
# 模糊匹配(包含站名)
for key, value in db.items():
if name in key or key in name:
return value
return None
def main() -> None:
parser = argparse.ArgumentParser(description="台灣加油站座標資料庫")
parser.add_argument("--download", action="store_true", help="下載 OSM 資料並建立資料庫")
parser.add_argument("--query", help="查詢加油站座標")
parser.add_argument("--city", default="", help="查詢時指定縣市")
args = parser.parse_args()
if args.download:
elements = download_osm_fuel_stations()
db = build_database(elements)
save_database(db)
print(f"總共 {len(db)} 個查詢鍵")
elif args.query:
db = load_database()
if not db:
print("資料庫不存在,請先執行 --download", file=__import__("sys").stderr)
return
result = query_coords(args.query, args.city, db)
if result:
print(f"座標: {result['lat']}, {result['lon']}")
else:
print(f"找不到: {args.query}")
else:
db = load_database()
if db:
print(f"資料庫包含 {len(db)} 個查詢鍵")
else:
print("資料庫不存在,請先執行 --download", file=__import__("sys").stderr)
if __name__ == "__main__":
main()