YouTube video catalog site Google Maps APIThis is a record of the implementation that later added the Maps JavaScript API / Geocoding API to link filming locations with town name labels on a map. The target site is GeoRunning Official Websiteの https://georunning.work/ This is written for people who are familiar with HTTP APIs and asynchronous processing on the front end, but are new to Maps.
First, we'll look at the basic structure of the catalog screen. Then, we'll proceed in the following order: which APIs were used for what purpose, saving locations, creating short addresses, and the public-facing map.
GeoRunning Official Website = YouTube video catalog
GeoRunning Official Websiteの https://georunning.work/ This is a catalog of publicly available videos from the YouTube channel "GeoRunning," as well as its sub-channels "Geo SUB-CH," "GeoAUTO," and "GeoBreathing." You can search for videos from all four channels (main and sub-channels) on the same screen. The CMS is Drupal. Video metadata is stored in a custom table and displayed as tiles at the top.

The basic structure of the screen is as follows:

- If you change the channel, sorting order, series, or keyword in step ②, the tiles in step ④ will be rearranged.
- ③ PANEL is a list of tiles, and MAP is a map showing locations with the same conditions.
- Clicking on address ⑤ will open a map showing only the location of that video (see below).
Use Muuri for tile placement
For tile positioning Muuri I use this because it rearranges the remaining tiles without any gaps each time I filter or sort.
The following are the decisions made by this website:
- I won't be using drag-and-drop sorting (because it's a catalog for browsing).
- The layout animation time is 0 (the animation is kept to a minimum due to the large number of items).
- Tiles that are not visible due to virtual scrolling are excluded from the layout.
- Channel, series, keyword, subtitle search, and various sorting options are determined outside of Muuri, and only the results are passed to the grid.
- If Muuri fails to initialize, it will drop the result into a CSS Grid. When switching to list view, Muuri will not be used, and the DOM will be arranged directly.
Muuri's only responsibility is "how to arrange the remaining cards." The website's logic determines "which cards to keep" and "in what order."
Things I added using the Google Maps API
The catalog allowed users to search for videos by title and thumbnail. What was missing was information about "where the video was filmed." A single video can have multiple filming locations. The list only shows the town name, not the street number. Clicking on an address opens only the location of that video, and the MAP allows users to view all the videos currently displayed in the list together.

We will use two APIs.
Of the Google Maps Platform components, the following two were used for this feature. It's easy to get this wrong when you're using Maps for the first time.
- Maps JavaScript API ...Draw a map on the browser. Add pins, zoom,
ROADMAP/HYBRIDClick here for Street View (Pegman) - Geocoding API ...Conversion of latitude/longitude and address string. This site uses a server
latlngThrow this and get a short Japanese label (reverse geocode).
Separate API keys for browsers and servers. Implement referrer restrictions (JS) and IP/API restrictions (server) for each.
Data flow (overall)
- In the management screen, you can save the latitude, longitude, location name, and order for each video.
- At the time of saving, the Geocoding API is called to create a short sentence containing "prefecture ~ town name" and save it together.
- The tiles on public pages only display the short text (they don't perform geocoding every time they are opened).
- Clicking on a tile's address → Displaying only "the location in that video" using the Maps JavaScript API.
- MAP tab → Display all locations of the videos currently listed using the same API.
The location is saved in the management screen.
For each video ID, the location will be stored on multiple lines. These lines will include latitude, longitude, location name, notes, and display order (sort_order). This display order will later become the 1st, 2nd, and 3rd locations on the map. Saving will be rejected if both latitude and longitude are 0, or if the location name is empty.
Each time you save, you run a reverse geocode and link the resulting short sentence to the location. The public page displays these saved short sentences as tiles and map tags.

In the example above, four locations are registered. The "Hide List" checkbox is used when you want to hide the location data but not display it on the catalog tiles.
Create labels down to the town name using the Geocoding API.
The Geocoding API also returns the full text for each coordinate, such as "1-1, Nunobiki-cho 4-chome, Chuo-ku, Kobe City, Hyogo Prefecture". This is too long to display below the tile. address_components The parts are loaded from the top, and those below the block level are dropped and stored.
Example (for explaining the method of segmentation; not the registered values for this station itself): Latitude 34.694839, Longitude 135.194942.language=ja、region=jp The full text returned is "1-1, Nunobiki-cho 4-chome, Chuo-ku, Kobe City, Hyogo Prefecture". The label left is "Nunobiki-cho, Chuo-ku, Kobe City, Hyogo Prefecture".
The following is a version where function names have been replaced with explanatory ones so that the implementation can be followed. In the comments, 【Google】 refers to general API procedures, and 【Policy】 refers to the rules for displaying and caching this site.
<?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 '';
}
To summarize the key points, there are three main ones.
- [Google]
geocode/jsonにlatlngHand it over,results[0].address_componentsRead - [Policy] Assemble the name up to the prefecture, city, and town level, then use regular expressions to remove everything after the initial "number + chome" part.
- [Policy] Cache the latitude and longitude for approximately 30 days using keys rounded to the fifth decimal place, and also remember empty results (to prevent repeated failures).
In cases where the location name is exactly the same string as the video title, it will not be displayed as an address label. This is because the repetition of the title would make it appear as a location. If necessary, a flag will be used to re-read the cache (above). $forceRefreshUse ).
Public pages read saved labels
When drawing tiles, the Geocoding API is not called every time they are displayed. For the first point, the label is determined in the following order:
// 擬似コード
if (保存済みの短文がある) {
タイル下段 = 保存済みの短文;
} else if (地点名があり、かつ動画タイトルと違う) {
タイル下段 = 地点名から「数字+丁目」以降を落としたもの;
} else {
タイル下段 = 出さない / GPS アイコンのみ;
}
At the same time, all locations in the video (latitude, longitude, and display address) are embedded as JSON on the tile side. The script that opens the map only needs to read this JSON.
Open the location map of the video from the address on the tile.
When you click the address below a tile in the PANEL, a modal opens and the Maps JavaScript API map plots only the locations in that video. It does not expand to the entire list. If there are two or more locations, they are numbered from 1 in the order they were saved. The pin and the tag below share the same number. If there is only one location, it is not numbered, and the zoom level immediately after opening is set to 15. If there are multiple locations, all locations are... fitBounds do.
// タイルから開く地図(その動画の地点だけ)
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);
The map type immediately after opening is HYBRID(Satellite image showing roads and place names). With a custom button. ROADMAP Go back and forth, and when you place the Pegman in the bottom right onto the road, it will switch to Street View. Press the number or pin. panTo Also, if the zoom level is less than 17, increase it to 17.
The text on the tags will use the saved display address first. Only empty locations will fall back to the browser-side Geocoder when the modal is opened. The bottom row of tiles themselves will not be re-rendered each time the modal is opened.
View all locations in a list on the MAP tab.
MAP is a separate map instance. Only locations attached to tiles currently visible on the PANEL are plotted. If a tile is hidden by searching or channel filtering, its pin will also be removed from the map. Clicking a pin will display the video card for that video on the map.
The locations are also outside the Kinki region and overseas. fitBounds This results in the desired area being a small cluster at the edge. The initial range uses only points that fall within the approximate rectangle of the Kinki region. Only expand to include all points if there are no points within the rectangle.
// 一覧全体の 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); // 街区屋根まで寄らない
});
The map type immediately after opening the MAP is ROADMAP That's it. The modal that opens from the tile HYBRID This is because the purpose of "viewing a single route including the terrain" and the purpose of "viewing the distribution of the entire catalog" are separated. Both allow you to switch between road maps, satellite images, and Street View.
When taking it to another catalog
- Save multiple latitude and longitude coordinates for each video, in an ordered manner.
- Use the Geocoding API when saving.
language=ja、region=jpCall only once - Stack prefectures, cities, towns, and villages, then drop everything after the first "number + chome" (district).
- Save short texts, and public tiles and maps will read them.
- The system memorizes the results using rounded latitude and longitude as keys, and also memorizes the results for the sky.
- The tile address will only show the location in the video with a number. Switch between map type and Street View.
- The map of the entire list is initially defined using only points within the area you want to display. The zoom limit is also determined.
This short text is used to indicate the general location in the list and on the map. A different string of text will be used for delivery addresses and screens that provide directions down to the street number.