42,000の山小屋と頂上のための安定したURL
Stable URLs for 42,000 Huts and Peaks (peter-hoffmann.com)
要約
biwak.infoは、OpenStreetMapのデータを元に、42,000以上の山小屋や頂上に安定したURLを生成するシステムを開発しました。名前の重複を解決するため、名称、地域、標高、OpenStreetMap IDを組み合わせたユニークなスラッグを生成し、過去に発行されたスラッグは変更しないレジストリで管理することで、共有されたリンクが壊れないようにしています。
全文翻訳
42,000の山小屋と頂上のための安定したURL
biwak.info は、私のアルプスの山小屋、ビバーク、頂上の地図です。長らく、山小屋へのリンク方法は、正確な名前の一致を行い、名前がユニークでない場合は検索にフォールバックする #Name ハッシュしかありませんでした。共有でき、チャットで適切なプレビューカードを表示できる実際のURLが欲しかったのです: biwak.info/p/matterhorn。
データは OpenStreetMap から来ています:9,429の山小屋とビバーク、33,437の命名された頂上、合計42,866の地点です。名前をURLに変換するのは簡単です。難しいのは、多くの地点が名前を共有しており、一度共有されたURLは、データが変更されても同じ地点を指し続ける必要があることです。
[画像: https://biwak.info/p/bivouac-des-periades のプレビューカード]
名前からスラッグへ
最初のステップは、プレーンな slugify です:ドイツ語のウムラウトは書き出され、それ以外はASCIIに折りたたまれ、小文字化され、その他の文字の連続は単一のダッシュになります。
GERMAN_TRANSLIT = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue", "Ä": "Ae", "Ö": "Oe", "Ü": "Ue",
"ß": "ss", "ẞ": "ss"})
def slugify(text):
"""ウムラウトとßのドイツ語転写、その後ASCIIフォールド、小文字化、その他すべてを単一ダッシュに変換します。"""
text = (text or "").translate(GERMAN_TRANSLIT)
text = unicodedata.normalize("NFKD", text).encode("ascii", "ignore").decode("ascii").lower()
return re.sub(r"[^a-z0-9]+", "-", text).strip("-")
したがって、Oberaletschhütte は oberaletschhuette に、Štuhecov dom は stuhecov-dom に、Rifugio Città di Busto は rifugio-citta-di-busto になります。42,866の名前のどれも空になりません。
名前はユニークではない
アルプスの名前は頻繁に繰り返されます。どの谷にもJagdhütteやRosskopfがあります。名前だけでは、最も一般的なスラッグは以下の通りです:
| スラッグ | 地点 |
|---|---|
jagdhuette | 70 |
schwarzkogel | 37 |
buchberg | 32 |
rosskopf | 32 |
mitterberg | 31 |
スキームを選択する前に、異なる接尾辞でまだスラッグを共有している地点がいくつあるかを測定しました:
| スラッグの構成要素 | 共有スラッグ | 影響を受ける地点 | 最悪ケース |
|---|---|---|---|
| 名前 | 2,181 | 6,690 (15.6 %) | 70 |
| 名前 + 国 | 1,993 | 5,936 (13.8 %) | 64 |
| 名前 + 地域 | 1,516 | 3,932 (9.2 %) | 27 |
| 名前 + 地域 + 標高 | 118 | 394 (0.9 %) | 21 |
国を追加してもほとんど助けになりません:ほとんどの重複は同じ国にあります。地域を追加すると約半分になりますが、それでもケルンテン州だけでJagdhütteという名前の地点が27あります。標高を追加すると残りのほとんどが解決されます。しかし、OpenStreetMapで標高が不明な山小屋が4,311あるため、すべてを解決することはできません。最後の行の最悪ケースは、リグーリア州のcasellaです:同じ名前で標高がない小さな山小屋が21あります。
したがって、常に機能する単一の接尾辞はなく、すべてを各URLに追加すると /p/matterhorn が /p/matterhorn-wallis-4478 のように見えてしまいます。
名前が共有されている間のみ接尾辞を追加する
私が決定したルールは次のとおりです:地点は、ユニークな最短のスラッグを取得します。以下の順序で試行します:
- プレーンな名前:
/p/matterhorn - 名前と地域:
/p/oberaletschhuetteはプレーンのままですが、共有名の場合は/p/rosskopf-tirolのようになります。 - 名前、地域、標高:
/p/rosskopf-tirol-2257 - 名前、地域、OpenStreetMap ID:
/p/jagdhuette-kaernten-w1202594655
コードでは、これは、同じ名前のスラッグを持つすべての現在の地点が与えられた場合に、1つの地点の候補を生成するジェネレーターです:
def candidates(p, group):
"""優先順位順のPOI pのスラッグ。`group` = 同じ名前のスラッグを持つ現在のすべてのPOI。"""
base, region = p["base"], slugify(p["region"])
others = [o for o in group if o["id"] != p["id"]]
if not others:
yield base
if not any(slugify(o["region"]) == region for o in others):
yield f"{base}-{region}"
if p["ele"] is not None and not any(slugify(o["region"]) == region and o["ele"] == p["ele"] for o in others):
yield f"{base}-{region}-{p['ele']}"
yield f"{base}-{region}-{p['id']}"
OpenStreetMap IDは、常にユニークですが、誰もそれを読むことができないため、最後の手段です。今日のデータでは、42,866の地点は次のように分割されています:
| スラッグ | 地点 |
|---|---|
| プレーンな名前 | 36,176 |
| + 地域 | 2,758 |
| + 標高 | 3,474 |
| + OpenStreetMap ID | 458 |
Natural Earthからの地域
地域は人々が認識できるものである必要があります。私は Natural Earth のadmin-1境界を、アルプスで意味のある単位に削減しました:オーストリアとドイツのBundesland、スイスのKanton、フランスのdépartement(SavoieとHaute-Savoieは大きなrégionよりもよく知られています)、イタリアのregione(South TyrolとTrentinoは分割)、スロベニアの統計地域です。名前は、ドイツ語が一般的な場合はドイツ語形式(Wallis、Tessin、Aostatal)を、それ以外の場合はローカルな形式を使用します。地図上の地点は、これらの63の地域に分類されます。
各地点は、Shapely を使用した点と多角形の検索によって地域を取得します。境界は簡略化されているため、境界上または湖岸にある山小屋は、すべての多角形の外側にある可能性があります。次に、最も近い多角形が勝ちます:
def lookup(self, lat, lon):
"""(cc, 地域名); すべての多角形のすぐ外側にある点(海岸、簡略化された境界)は最も近い点を使用します。"""
pt = Point(lon, lat)
for i in self.tree.query(pt):
if self.geoms[i].contains(pt):
break
else:
i = self.tree.nearest(pt)
return self.props[i]["cc"], self.props[i]["name"]
地域は、プレビューカードと詳細ビューでも、タイプに隣接して表示されます:「Biwak · Haute-Savoie, Frankreich」。
安定性:決して忘れないレジストリ
上記のスキームはデータに依存します。明日誰かがOpenStreetMapに2番目のMatterhornを追加した場合、プレーンなルールは実際のものに matterhorn-wallis という名前を付け直し、これまでに共有されたすべてのリンクは壊れます。
そのため、ビルドは スラッグレジストリ data/slug-registry.json を保持し、gitにコミットされます。これは、これまで発行されたすべてスラッグをOpenStreetMap IDと名前にマッピングします:
"matterhorn": ["n26863664", "Matterhorn"],
"oberaletschhuette": ["n11262553552", "Oberaletschhütte"],
割り当ては次の3つのルールに従います:
- レジストリ内のスラッグは決して別の地点に移動しません。
- 地点は、現在の名前(たとえプレーンなルールが別の名前を付けるとしても)に適合し続ける限り、レジストリスラッグを保持します。2番目のMatterhornが登場した場合、古いものは
matterhornのままで、新しいものだけが接尾辞を取得します。 - 新しく作成された、または名前が変更された 地点は、まだ使用されていない最初候補を取得します。名前が変更された地点の古いスラッグはレジストリに残され、新しい地点にリダイレクトされます。
primary = {}
for p in pois:
fitting = [s for s in by_id.get(p["id"], []) if s == p["base"] or s.startswith(p["base"] + "-")]
if fitting:
primary[p["id"]] = min(fitting, key=lambda s: (len(s), s))
for base in sorted(groups):
for p in groups[base]:
if p["id"] in primary:
continue
slug = next(s for s in candidates(p, groups[base]) if s not in registry)
registry[slug] = [p["id"], p["name"]]
primary[p["id"]] = slug
OpenStreetMapから消えた地点は、スラッグを予約したままにします。それを開くと、誰かの山小屋を表示する代わりに、古い名前の検索が開始されます。
レジストリは1行あたり1つのスラッグを持つソートされたJSONファイルであるため、スラッグを変更するビルドは読みやすいgit diffとして表示されます。これにより、1つの間違いが簡単に特定できました:最初のバージョンでは ü を u に折りたたんだため、Oberaletschhütte は /p/oberaletschhutte でした。ローンチから2週間後、私は ue に切り替え、7,182のスラッグが変更されました。レジストリをゼロから再構築したため、古いスラッグはリダイレクトされる代わりに削除されました。
2つのフォールバック
/p/rosskopf のように共有されているプレーンな名前は、エラーを返しません。その名前の地点だけを検索結果として表示し、タイプ、標高、地域を示し、それぞれが自身のスラッグにリンクします。/p/rosskopf-tirol の場合も同様です。
OpenStreetMap IDで終わるスラッグは、名前の部分が間違っているか古い場合でも、常に解決します。/p/old-name-n11262553552 は /p/oberaletschhuette への301リダイレクトで応答します。
制限事項
- 読みやすいことがユニークであることを意味するわけではありません。458の地点はスラッグにOpenStreetMap IDで終わります。OpenStreetMapでの標高データの改善は、その数を減らすでしょう。レジストリは古いスラッグをリダイレクトとして機能させ続けます。
- レジストリは増え続けるだけです。これまで発行されたスラッグごとに1行あり、現在は42,866行です。
- ルールを変更すると数千のURLに影響します。
slugifyまたは地域の変更は、ウムラウトの切り替えが示したように、多くの地点の名前を変更します。
同じ /p/ URLは、Loading 33,000 Peaks on Demand with a z10 Tile Grid で説明されているように、地図が頂上を直接開くことも可能にします。