PageSourceSearch

https://tozan.pages.dev/js/search.js

js tozan.pages.dev collected 2026-10-03 06:00:49 UTC 2,898 bytes, 80 lines download raw bytes

1// 地名・住所検索(Nominatim forward geocoding)。ボタンから左に検索窓を展開し、
2// 候補クリックでその場所へflyToする。ポップアップの実名表示(popup.js)と同じ
3// Nominatimを使うが、そちらは逆ジオコーディング(緯度経度→名称)でこちらは
4// 順ジオコーディング(地名→緯度経度)という違いがある。
5const DEBOUNCE_MS = 400;
6
7export function initSearch(map) {
8  const btn = document.getElementById('search-toggle');
9  const panel = document.getElementById('search-panel');
10  const input = document.getElementById('search-input');
11  const resultsEl = document.getElementById('search-results');
12
13  let debounceTimer = null;
14  let currentToken = 0;
15
16  function openPanel() {
17    panel.classList.add('visible');
18    btn.classList.add('on');
19    input.focus();
20  }
21  function closePanel() {
22    panel.classList.remove('visible');
23    btn.classList.remove('on');
24    input.value = '';
25    resultsEl.innerHTML = '';
26  }
27
28  btn.addEventListener('click', (e) => {
29    e.stopPropagation();
30    if (panel.classList.contains('visible')) closePanel();
31    else openPanel();
32  });
33
34  // パネル外クリックで閉じる
35  document.addEventListener('click', (e) => {
36    if (!panel.classList.contains('visible')) return;
37    if (panel.contains(e.target)) return;
38    closePanel();
39  });
40
41  input.addEventListener('keydown', (e) => {
42    if (e.key === 'Escape') closePanel();
43  });
44
45  input.addEventListener('input', () => {
46    clearTimeout(debounceTimer);
47    const q = input.value.trim();
48    if (!q) { resultsEl.innerHTML = ''; return; }
49    debounceTimer = setTimeout(() => runSearch(q), DEBOUNCE_MS);
50  });
51
52  async function runSearch(q) {
53    const myToken = ++currentToken;
54    resultsEl.innerHTML = '<div class="search-result-empty">検索中...</div>';
55    let data;
56    try {
57      const url = `https://nominatim.openstreetmap.org/search?format=jsonv2&q=${encodeURIComponent(q)}&countrycodes=jp&accept-language=ja&limit=6`;
58      data = await fetch(url).then(r => r.json());
59    } catch (e) {
60      if (myToken === currentToken) resultsEl.innerHTML = '<div class="search-result-empty">検索に失敗しました</div>';
61      return;
62    }
63    if (myToken !== currentToken) return;  // 入力中に古い検索結果が返ってきた場合は破棄
64    if (!data.length) {
65      resultsEl.innerHTML = '<div class="search-result-empty">見つかりませんでした</div>';
66      return;
67    }
68    resultsEl.innerHTML = '';
69    for (const d of data) {
70      const item = document.createElement('div');
71      item.className = 'search-result-item';
72      item.textContent = d.display_name;
73      item.addEventListener('click', () => {
74        map.flyTo({ center: [+d.lon, +d.lat], zoom: 14, duration: 1200 });
75        closePanel();
76      });
77      resultsEl.appendChild(item);
78    }
79  }
80}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.