YouTube 動画のカタログサイトに Google Maps API(Maps JavaScript API / Geocoding API)を後から入れ、ロケ地点を町名ラベルと地図で連動させた実装の記録だ。対象サイトは GeoRunning 公式Webサイトの https://georunning.work/ 。HTTP API やフロントの非同期処理には慣れているが、Maps はこれから、という人向けに書く。
先にカタログ画面の骨格を見る。続けて、どの API を何に使ったか、地点の保存、短い住所の作り方、公開側の地図、の順で進める。
GeoRunning 公式Webサイト = YouTube 動画カタログ
GeoRunning 公式Webサイトの https://georunning.work/ は、YouTube チャンネルの「GeoRunning」をはじめ、サブチャンネルの「Geo SUB-CH」、「GeoAUTO」、「GeoBreathing」の公開動画カタログだ。メイン・サブチャンネルの計4チャンネルの公会動画を同じ画面で探せる。CMS は Drupal。動画のメタデータは自前のテーブルに持ち、トップでタイルとして並べている。

画面の骨格は、次のとおりだ。

- ②でチャンネル・並び順・シリーズ・キーワードを変えると、④のタイルが入れ替わる
- ③の PANEL がタイル一覧、MAP が同じ条件の地点を地図にしたもの
- ⑤の住所を押すと、その動画だけの地点地図が開く(後述)
タイル配置に Muuri を使う
タイルの位置決めには Muuri を使っている。フィルタやソートのたびに、残ったタイルを隙間なく並べ直すためだ。
このサイト側で決めたことは、次の程度だ。
- ドラッグ並べ替えはしない(閲覧用カタログだから)
- レイアウトのアニメーション時間は 0(件数が多いので動きを抑える)
- 仮想スクロールで見えていないタイルは、レイアウト対象から外す
- チャンネル・シリーズ・キーワード・字幕検索・各種ソートは Muuri の外で判定し、結果だけをグリッドへ渡す
- Muuri が初期化できなかったときは CSS Grid に落とす。リスト表示に切り替えたときは Muuri を使わず DOM を直接並べる
Muuri は「残ったカードをどう並べるか」だけを担当する。「どれを残すか」「どの順か」はサイト側のロジックが決める。
Google Maps API で足したこと
カタログとしては、タイトルとサムネイルで動画を探せた。足りなかったのは「その動画がどこで撮られたか」だ。ロケは1本に複数ある。一覧では町名までを見せ、番地までは出さない。住所を押したらその動画の地点だけを開き、MAP ではいま一覧に出ている動画をまとめて見られるようにした。

使う API は二つ
Google Maps Platform のうち、この機能に使ったのは次の二つだ。初めて Maps を触るときは、ここを取り違えやすい。
- Maps JavaScript API … ブラウザ上に地図を描く。ピン、ズーム、
ROADMAP/HYBRID、ストリートビュー(ペグマン)はこちら - Geocoding API … 緯度経度と住所文字列の変換。このサイトではサーバから
latlngを投げ、日本語の短いラベルを得る(逆ジオコード)
API キーはブラウザ用とサーバ用で分ける。リファラ制限(JS)と IP / API 制限(サーバ)をそれぞれ付ける。
データの流れ(全体)
- 管理画面で、動画ごとに緯度・経度・地点名・順序を保存する
- 保存のタイミングで Geocoding API を呼び、「都道府県〜町名」の短文を作って一緒に保存する
- 公開ページのタイルは、その短文を読むだけ(開くたびに Geocoding しない)
- タイルの住所クリック → Maps JavaScript API で「その動画の地点」だけを表示
- MAP タブ → 同じ API で「いま一覧に出ている動画の地点」をまとめて表示
地点は管理画面で保存する
動画 ID に対して、地点を複数行持たせる。持つのは緯度、経度、地点名、メモ、表示順(sort_order)だ。この表示順が、あとで地図上の 1、2、3 になる。緯度経度が両方 0、または地点名が空の保存は拒否する。
保存のたびに逆ジオコードを走らせ、できた短文を地点に紐づけて置く。公開ページがタイルや地図の札に出すのは、この保存済み短文だ。

上の例では 4 地点が登録されている。リスト非表示のチェックは、カタログのタイルには出さず、地点データだけ残したいときに使う。
Geocoding API で町名までのラベルを作る
Geocoding API は、座標に対して「兵庫県神戸市中央区布引町四丁目1番1号」のような全文も返す。タイルの下に出すには長い。そこで address_components から部品を積み、丁目より下を落として保存する。
例(切り方の説明用。この駅そのものの登録値ではない):緯度 34.694839、経度 135.194942。language=ja、region=jp で返る全文は「兵庫県神戸市中央区布引町四丁目1番1号」。残すラベルは「兵庫県神戸市中央区布引町」。
以下は、実装を追えるように関数名を解説用へ置き換えた版だ。コメントの【Google】は API の一般的な手続き、【方針】はこのサイトの表示・キャッシュの決めごとだ。
<?php
// 解説用に関数名・定数名を置き換えた版。
// 【Google】= Geocoding API の一般的な使い方
// 【方針】 = このサイトで決めた表示ルール / キャッシュ方針
const TOWN_LABEL_CACHE_SECONDS = 86400 * 30; // 【方針】約30日
/**
* 緯度経度 → 「都道府県〜町名」までの短いラベル。
* 丁目・番地・号は出さない。
*/
function buildTownLevelLabel(float $lat, float $lon, bool $forceRefresh = false): string
{
// 【方針】1m弱の差は同じ地点として扱う
$latKey = round($lat, 5);
$lonKey = round($lon, 5);
$cacheId = 'town_label_v3:' . hash('sha256', $latKey . ',' . $lonKey);
if (!$forceRefresh) {
$cached = cacheGet($cacheId); // Drupal cache など
if (is_string($cached) && $cached !== '') {
return $cached === '__EMPTY__' ? '' : $cached;
}
}
$apiKey = getMapsApiKey(); // サーバ側で保持
if ($apiKey === '') {
return '';
}
// 【Google】逆ジオコード(latlng)
$url = 'https://maps.googleapis.com/maps/api/geocode/json?' . http_build_query([
'latlng' => sprintf('%F,%F', $lat, $lon),
'language' => 'ja', // 【方針】日本語ラベルが欲しい
'region' => 'jp', // 【方針】日本の解釈を優先
'key' => $apiKey,
], '', '&', PHP_QUERY_RFC3986);
$payload = httpGet($url);
$decoded = json_decode($payload, true);
if (!is_array($decoded)) {
return '';
}
$status = $decoded['status'] ?? '';
if ($status !== 'OK') {
// 【方針】失敗もキャッシュし、空振りの連続リクエストを防ぐ
cacheSet($cacheId, '__EMPTY__', TOWN_LABEL_CACHE_SECONDS);
return '';
}
$components = $decoded['results'][0]['address_components'] ?? [];
$label = composeTownLabelFromComponents($components);
cacheSet(
$cacheId,
$label !== '' ? $label : '__EMPTY__',
TOWN_LABEL_CACHE_SECONDS
);
return $label;
}
/**
* address_components から表示用短文を組む。
* Google は「全文」も返すが、一覧には長すぎるので使わない。【方針】
*/
function composeTownLabelFromComponents(array $components): string
{
$prefecture = firstLongName($components, 'administrative_area_level_1');
$locality = firstLongName($components, 'locality'); // 市など
$ward = firstLongName($components, 'sublocality_level_1'); // 区など
$county = firstLongName($components, 'administrative_area_level_2');
// 市区の骨格
$base = '';
if ($locality !== '' && $ward !== '') {
$base = (mb_strpos($locality, $ward) !== false)
? $locality
: ($locality . $ward);
} elseif ($locality !== '') {
$base = $locality;
} elseif ($county !== '') {
// 郡・支庁など locality が薄いケースのフォールバック
$base = $county;
}
if ($base === '') {
$base = $prefecture;
}
// 町名・字相当を足す(番地は足さない)
$withTown = appendNeighborhood($base, $components);
// 都道府県が頭に無ければ付ける
$withPref = ensurePrefecturePrefix($withTown, $prefecture);
// 「数字+丁目」以降を落とす
return stripFromChomeOnward($withPref);
}
function appendNeighborhood(string $base, array $components): string
{
$types = [
'neighborhood',
'sublocality_level_2',
'sublocality_level_3',
'sublocality_level_4',
'sublocality_level_5',
];
$parts = [];
foreach ($types as $type) {
$name = firstLongName($components, $type);
if ($name === '' || mb_strpos($base, $name) !== false) {
continue;
}
$parts[] = $name;
}
return $base . implode('', $parts);
}
function ensurePrefecturePrefix(string $base, string $prefecture): string
{
$base = trim($base);
$prefecture = trim($prefecture);
if ($base === '') {
return $prefecture;
}
if ($prefecture === '' || mb_strpos($base, $prefecture) === 0) {
return $base;
}
return $prefecture . $base;
}
/** 【方針】最初の「◯丁目」とその後ろを捨てる。丁目表記が無い数字はそのまま残る */
function stripFromChomeOnward(string $s): string
{
$s = trim($s);
if ($s === '') {
return '';
}
return trim(preg_replace('/[0-9]+丁目.*$/us', '', $s) ?? $s);
}
function firstLongName(array $components, string $wantType): string
{
foreach ($components as $c) {
$types = $c['types'] ?? [];
if (!is_array($types) || !in_array($wantType, $types, true)) {
continue;
}
$long = trim((string) ($c['long_name'] ?? ''));
if ($long !== '') {
return $long;
}
}
return '';
}
ポイントだけ抜き出すと、次の三つだ。
- 【Google】
geocode/jsonにlatlngを渡し、results[0].address_componentsを読む - 【方針】都道府県・市区・町名までを組み立て、最初の「数字+丁目」以降を正規表現で落とす
- 【方針】緯度経度を小数第5位で丸めたキーで約30日キャッシュし、空結果も覚える(失敗の連打を防ぐ)
地点名が動画タイトルとまったく同じ文字列だけ、というケースでは、それを住所ラベルとしては出さない。タイトルの繰り返しが、場所に見えてしまうためだ。必要ならキャッシュを読まずに取り直すフラグ(上の $forceRefresh)を使う。
公開ページは保存済みラベルを読む
タイルを描くとき、表示のたびに Geocoding API は呼ばない。先頭の地点について、次の順でラベルを決める。
// 擬似コード
if (保存済みの短文がある) {
タイル下段 = 保存済みの短文;
} else if (地点名があり、かつ動画タイトルと違う) {
タイル下段 = 地点名から「数字+丁目」以降を落としたもの;
} else {
タイル下段 = 出さない / GPS アイコンのみ;
}
同時に、その動画の全地点(緯度経度と表示用住所)を JSON としてタイル側に埋め込んでおく。地図を開くスクリプトは、この JSON だけを読めばよい。
タイルの住所から、その動画の地点地図を開く
PANEL でタイル下の住所を押すと、モーダルが開き、Maps JavaScript API の地図がその動画の地点だけをプロットする。一覧全体には広げない。地点が2件以上なら、保存順に 1 から番号を振る。ピンと下の札が同じ番号を共有する。1件だけのときは番号を付けず、開いた直後のズームを 15 にする。複数件なら全地点に fitBounds する。
// タイルから開く地図(その動画の地点だけ)
const map = new google.maps.Map(canvas, {
zoom: 13,
center: { lat: 34.678541, lng: 135.010225 }, // 初期値。直後に fitBounds する
mapTypeId: google.maps.MapTypeId.HYBRID, // 衛星 + 道路・地名
streetViewControl: true,
mapTypeControl: false, // 標準UIは出さず、自前ボタンで切替
});
// 道路地図 ↔ 衛星(ハイブリッド)
map.setMapTypeId(
isRoadmapNow
? google.maps.MapTypeId.HYBRID
: google.maps.MapTypeId.ROADMAP
);
// 番号札 / ピン押下 → その地点へ
map.panTo(marker.getPosition());
const z = map.getZoom();
map.setZoom(typeof z === 'number' ? Math.max(z, 17) : 17);
開いた直後の地図種別は HYBRID(衛星写真に道路と地名)。自前ボタンで ROADMAP と往復し、右下のペグマンを道へ置くとストリートビューになる。番号かピンを押すと panTo し、ズームが 17 未満なら 17 まで上げる。
札の文言は、保存済みの表示用住所を先に使う。空の地点だけ、モーダルを開いたタイミングでブラウザ側 Geocoder にフォールバックする。タイル下段そのものは、開くたびに取り直さない。
MAP タブで一覧の地点をまとめて見る
MAP は別の地図インスタンスだ。プロット対象は、「いま PANEL 側で見えているタイル」に付いた地点だけ。検索やチャンネル絞り込みでタイルが隠れれば、そのピンも地図から外れる。ピンを押すと、その動画のカードを地図の上に出す。
地点は近畿の外や海外にもある。全ピンに fitBounds すると、見たい地域が端の小さな塊になる。最初の範囲は近畿のおおよその矩形に入る点だけを使う。矩形内に点が無いときだけ全点に広げる。
// 一覧全体の MAP(いま見えているタイルの地点だけ)
const KANSAI = { south: 33.4, west: 134.5, north: 35.8, east: 136.9 };
function pickInitialPoints(allPoints) {
const inKansai = allPoints.filter((p) =>
p.lat >= KANSAI.south && p.lat <= KANSAI.north &&
p.lng >= KANSAI.west && p.lng <= KANSAI.east
);
// 近畿に1点も無ければ全点(海外だけ、など)
return inKansai.length > 0 ? inKansai : allPoints;
}
map.fitBounds(boundsFrom(pickInitialPoints(allPoints)), 40);
google.maps.event.addListenerOnce(map, 'idle', () => {
if (map.getZoom() > 16) map.setZoom(16); // 街区屋根まで寄らない
});
MAP を開いた直後の地図種別は ROADMAP だ。タイルから開くモーダルが HYBRID なのは、「1本のルートを地形込みで見る」用途と、「カタログ全体の分布を見る」用途を分けたためだ。どちらも道路地図 / 衛星写真 / ストリートビューを切り替えられる。
別のカタログへ持ち出すとき
- 動画ごとに緯度経度を複数、順序付きで保存する
- 保存時に Geocoding API を
language=ja、region=jpで一度だけ呼ぶ - 都道府県・市区町村・町名を積み、最初の「数字+丁目」以降を落とす
- 短文を保存し、公開のタイルと地図はそれを読む
- 丸めた緯度経度をキーに結果を覚え、空の結果も覚える
- タイルの住所ではその動画の地点だけを番号付きで出す。地図種別とストリートビューを切り替える
- 一覧全体の地図は、見せたい地域の内側の点だけで初期範囲を決める。ズーム上限も決める
この短文は、一覧と地図で場所のあたりを示すためのものだ。配送先や、現地で番地まで案内する画面には、別の文字列を使う。