/** * 一覧・詳細・地図で共通の住所解決(正: prefecture + city + town)。 * Node(generate-test-static.mjs)では require(.cjs)、ブラウザでは RakusumoPropertyAddress として利用。 * * SYNC: php-backend/lib/address_normalize.php とアルゴリズムを揃えること。 */ (function (root, factory) { if (typeof module === 'object' && module.exports) { module.exports = factory(); } else { root.RakusumoPropertyAddress = factory(); } })(typeof globalThis !== 'undefined' ? globalThis : this, function () { 'use strict'; function text(v, fb) { var s = String(v == null ? '' : v).trim(); if (s) return s; return fb == null ? '' : String(fb); } function propertyRawDataEntry(row) { var facilities = Array.isArray(row && row.facilities) ? row.facilities : []; for (var i = 0; i < facilities.length; i++) { var x = facilities[i]; if (x && typeof x === 'object' && x.__kind === 'raw_data') return x; } return null; } function concatPt(p, c, t) { return text(p) + text(c) + text(t); } /** * prefecture / city / town の解決(地図・住所1行の共通ソース)。 * 優先順位: * 1. row.prefecture + row.city + row.town(いずれかがあればこのセットを採用し、下位ソースは使わない) * 2. raw.都道府県 + raw.市区町村 + raw.町名番地 * 3. normalized.prefecture + normalized.city + normalized.town * 4. normalized.town(町名番地のみの補完) * 5. address / 所在地 のパースで穴埋め */ function pickAddressTriplet(pref, city, town) { var P = text(pref); var C = text(city); var T = text(town); if (!P && !C && !T) return null; return { prefecture: P, city: C, town: T }; } function resolvePartsFromRow(row) { var entry = propertyRawDataEntry(row); var raw = entry && entry.raw && typeof entry.raw === 'object' ? entry.raw : {}; var norm = entry && entry.normalized && typeof entry.normalized === 'object' ? entry.normalized : {}; var parts = pickAddressTriplet(row.prefecture, row.city, row.town) || pickAddressTriplet(raw['都道府県'], raw['市区町村'], raw['町名番地']) || pickAddressTriplet(norm.prefecture, norm.city, norm.town); var p = ''; var c = ''; var tt = ''; if (parts) { p = parts.prefecture; c = parts.city; tt = parts.town; } if (!text(tt)) { tt = text(norm.town); } var addrLine = text(row.address); var sh = text(row['所在地']); var line = addrLine || sh; var parsed = parseAddressLineJa(line); if (!p) p = parsed.prefecture; if (!c) c = parsed.city; if (!tt) tt = parsed.town; if (text(parsed.town).length > text(tt).length) { tt = parsed.town; if (!c && parsed.city) c = parsed.city; if (!p && parsed.prefecture) p = parsed.prefecture; } if (!p && c) p = inferPrefectureFromCity(c, line); return { prefecture: p, city: c, town: tt, addressLine: addrLine, shozaichi: sh }; } var KANAGAWA_CITY_RE = /^(横浜市|川崎市|相模原市|横須賀市|平塚市|鎌倉市|藤沢市|小田原市|茅ヶ崎市|逗子市|三浦市|秦野市|厚木市|大和市|伊勢原市|海老名市|座間市|南足柄市|綾瀬市)/u; function inferPrefectureFromCity(city, fullLine) { var c = text(city); var line = String(fullLine || '').replace(/\s+/g, ''); if (KANAGAWA_CITY_RE.test(c) || KANAGAWA_CITY_RE.test(line)) return '神奈川県'; return ''; } function parseAddressLineJa(address) { var line = String(address || '').trim().replace(/\s+/g, ' '); var compact = line.replace(/\s+/g, ''); if (!compact) return { prefecture: '', city: '', town: '' }; var prefecture = ''; var rest = compact; var pm = compact.match(/^(東京都|北海道|(?:京都|大阪)府|.{2,3}県)(.+)$/u); if (pm) { prefecture = pm[1]; rest = pm[2]; } else if (KANAGAWA_CITY_RE.test(compact)) { prefecture = '神奈川県'; rest = compact; } var ward = rest.match(/^(.+区)(.+)$/u); if (ward) { return { prefecture: prefecture, city: ward[1], town: String(ward[2] || '').trim() }; } var m2 = rest.match(/^(.+?[市区町村])(.*)$/u); var city = m2 ? m2[1] : ''; var town = m2 ? String(m2[2] || '').trim() : rest; return { prefecture: prefecture, city: city, town: town }; } function addressDetailScore(s) { if (!s) return 0; var t = String(s).replace(/\s/g, ''); var score = t.length; if (/[0-90-9].*[0-90-9]/.test(t)) score += 20; if (/[--‐ー〜~]/.test(t)) score += 10; if (/番地|丁目|号/.test(t)) score += 15; return score; } function pickRicherAddressLine(a, b) { if (!a) return b || ''; if (!b) return a; if (b.indexOf(a) === 0 && b.length > a.length) return b; if (a.indexOf(b) === 0 && a.length > b.length) return a; var ac = a.replace(/\s/g, ''); var bc = b.replace(/\s/g, ''); if (bc.indexOf(ac) === 0 && bc.length > ac.length) return b; if (ac.indexOf(bc) === 0 && ac.length > bc.length) return a; return addressDetailScore(b) > addressDetailScore(a) ? b : a; } /** 町名側が pref / city / pref+city で始まる冗長さを剥がす(結合前の town 用) */ function stripLeadingRedundantAddressTail(p, c, t) { p = text(p); c = text(c); t = text(t); var pc = p + c; var guard = 0; while (guard++ < 20 && t) { var before = t; if (pc && t.indexOf(pc) === 0) { t = t.slice(pc.length); } else if (c && t.indexOf(c) === 0) { t = t.slice(c.length); } else if (p && t.indexOf(p) === 0) { t = t.slice(p.length); } else { break; } t = text(t); if (t === before) break; } return t; } function collapseConsecutiveSegment(compact, seg) { if (!seg || seg.length < 2) return compact; var dup = seg + seg; var guard = 0; while (guard++ < 50 && compact.indexOf(dup) !== -1) { compact = compact.split(dup).join(seg); } return compact; } /** * 公開表示用1行(空白除去後、pref+city / city の連続重複を潰す)。 * partsHint は resolvePartsFromRow の prefecture / city を渡す。 */ function formatDisplayAddress(line, partsHint) { var compact = String(line || '').replace(/\s+/g, '').trim(); if (!compact) return ''; var p = partsHint && text(partsHint.prefecture); var c = partsHint && text(partsHint.city); var pc = p + c; if (pc.length >= c.length) compact = collapseConsecutiveSegment(compact, pc); if (c) compact = collapseConsecutiveSegment(compact, c); if (p) compact = collapseConsecutiveSegment(compact, p); return compact; } /** * 正規化済み1行(pref+city+town を主とし、address / 所在地 の枝番を取り込む)。 * 保存データは変えず、表示向けに連続する市区町村などを整理する。 */ function getCanonicalAddressLine(row) { if (!row || typeof row !== 'object') return ''; var parts = resolvePartsFromRow(row); var p = parts.prefecture; var c = parts.city; var townStripped = stripLeadingRedundantAddressTail(p, c, parts.town); var concat = concatPt(p, c, townStripped); var addr = parts.addressLine; var sh = parts.shozaichi; var base = concat || addr || sh; var alt = pickRicherAddressLine(addr, sh); var merged = pickRicherAddressLine(base, alt) || base; return formatDisplayAddress(merged, { prefecture: p, city: c }); } /** * Google マップ検索用文字列(getCanonicalAddressLine と同一。将来分岐する場合のエイリアス) */ function getMapAddress(row) { return getCanonicalAddressLine(row); } return { text: text, propertyRawDataEntry: propertyRawDataEntry, resolvePartsFromRow: resolvePartsFromRow, parseAddressLineJa: parseAddressLineJa, formatDisplayAddress: formatDisplayAddress, getCanonicalAddressLine: getCanonicalAddressLine, getMapAddress: getMapAddress, }; });