import { createNameDebug } from "./names.js"; import { CELL_SIZE, INF, MAP_H, MAP_W, MinHeap, clamp, indexOf, inside, rand, xyOf } from "./mapUtils.js"; import { applyOutputOptions, attachIdsAndNames, influenceFromPaths, tagInsidePrefecture } from "./mapGeneratorHelpers.js"; import { reconcileMunicipalMetadata } from "./mapMunicipalCoherence.js"; import { routeQualityAcceptable } from "./mapTransport.js"; const MUNICIPAL_SUFFIX_RE = /[市町村区]$/u; function stripMunicipalSuffix(name) { return String(name || "").replace(/[市町村区]$/u, "").trim(); } function municipalitySuffixForCenter(center, fields, seed, ordinal = 0) { const i = inside(center?.x || 0, center?.y || 0) ? indexOf(center.x, center.y) : 0; const density = fields.populationDensity?.[i] || 0; const land = fields.landuse?.[i] ?? 0; const urban = density > 0.36 || [2, 3, 4, 7, 8].includes(land) || center?.protectedSatellite; const rural = (fields.elevation?.[i] || 0) > 0.58 || (fields.slope?.[i] || 0) > 0.40 || (fields.ridgeField?.[i] || 0) > 0.46; if (urban) return "市"; if (rural && rand(seed + ordinal * 17, 9021) < 0.58) return "村"; return "町"; } function municipalityNameFromRoot(root, center, fields, seed, ordinal = 0) { let value = String(root || center?.name || "").trim(); if (!value) value = `自治${ordinal + 1}`; value = value.replace(/[市町村区駅港城跡宿]$/gu, ""); const fallback = String(center?.generatedMunicipalityName || "里").replace(/[市町村区駅港城跡宿]$/gu, ""); if (!value) value = fallback || "里"; return `${value}${municipalitySuffixForCenter(center, fields, seed, ordinal)}`; } function centerMunicipalityId(center, fallback = -1) { for (const key of ["adminId", "municipalityId", "adminNumericId"]) { const value = center?.[key]; if (Number.isFinite(value) && value >= 0) return Math.floor(value); } return fallback; } function assignMunicipalityPopulations(adminCenters, adminId, fields, settlementFeatures = []) { if (!adminCenters?.length || !adminId) return; const centerById = new Map(); let maxId = -1; for (const center of adminCenters) { const id = centerMunicipalityId(center); if (id >= 0 && !centerById.has(id)) { centerById.set(id, center); maxId = Math.max(maxId, id); } } for (let i = 0; i < adminId.length; i++) if (adminId[i] >= 0) maxId = Math.max(maxId, adminId[i]); const totals = new Float64Array(maxId + 1); const settlementTotals = new Float64Array(maxId + 1); const landCells = new Uint32Array(maxId + 1); const inhabitedCells = new Uint32Array(maxId + 1); for (let i = 0; i < adminId.length; i++) { const id = adminId[i]; if (id < 0 || fields.sea?.[i]) continue; landCells[id]++; const density = fields.populationDensity?.[i] || 0; const lu = fields.landuse?.[i] ?? 0; const plain = fields.plain?.[i] || 0; const agri = fields.agriculture?.[i] || 0; const builtWeight = lu === 3 ? 520 : lu === 2 ? 360 : lu === 4 || lu === 7 || lu === 8 ? 260 : lu === 5 || lu === 6 ? 160 : lu === 1 ? 82 : 22; const ruralFloor = lu === 1 ? 10 + agri * 18 + plain * 10 : 0; if (density > 0.006 || ruralFloor > 0 || lu > 0) inhabitedCells[id]++; totals[id] += density * builtWeight + ruralFloor; } // Population-bearing generated settlements are canonical entities, so add // their explicit populations exactly once to the municipality containing the // point. Some towns are promoted to cities later, so the same coordinate can // appear in both `markets` and `modernCities`; keep only the strongest record // per coordinate to avoid double counting. const uniqueSettlementByCell = new Map(); for (const feature of settlementFeatures || []) { if (!feature || !Number.isFinite(feature.population) || feature.population <= 0) continue; if (!inside(feature.x, feature.y)) continue; const i = indexOf(feature.x, feature.y); if (fields.sea?.[i]) continue; const key = `${Math.round(feature.x)}:${Math.round(feature.y)}`; const current = uniqueSettlementByCell.get(key); const priority = (feature.isPrefecturalCapital ? 4_000_000 : 0) + (feature.isRegionalCapital ? 1_000_000 : 0) + (feature.population || 0); if (!current || priority > current.priority) uniqueSettlementByCell.set(key, { feature, priority, i }); } let skippedDuplicateSettlementPopulation = 0; for (const { feature, i } of uniqueSettlementByCell.values()) { const id = adminId[i]; if (id < 0 || id >= settlementTotals.length) continue; settlementTotals[id] += feature.population; } for (const feature of settlementFeatures || []) { if (!feature || !Number.isFinite(feature.population) || feature.population <= 0 || !inside(feature.x, feature.y)) continue; const key = `${Math.round(feature.x)}:${Math.round(feature.y)}`; const kept = uniqueSettlementByCell.get(key)?.feature; if (kept && kept !== feature) skippedDuplicateSettlementPopulation += feature.population || 0; } for (const [id, center] of centerById) { const raw = (totals[id] || 0) + (settlementTotals[id] || 0); const minimumResidentPopulation = landCells[id] > 0 ? Math.round(Math.max(100, Math.min(3800, 80 + landCells[id] * 9 + inhabitedCells[id] * 16)) / 100) * 100 : 0; const adjustedRaw = Math.max(raw, minimumResidentPopulation); const rounded = adjustedRaw >= 10000 ? Math.round(adjustedRaw / 1000) * 1000 : Math.max(minimumResidentPopulation, Math.round(adjustedRaw / 100) * 100); const safePopulation = Math.max(landCells[id] > 0 ? 100 : 0, rounded); center.municipalityPopulation = safePopulation; // Some consumers still read the generic `population` field from municipal // centers. Mirror the municipality total there so no municipality is shown // as 0人 merely because it is not a canonical city/market entity. center.population = Math.max(center.population || 0, safePopulation); center.municipalitySettlementPopulation = Math.max(0, Math.round((settlementTotals[id] || 0) / 100) * 100); center.municipalitySkippedDuplicateSettlementPopulation = Math.max(0, Math.round(skippedDuplicateSettlementPopulation / 100) * 100); } } function promotePrefectureCapitalPopulations(prefectureRegionId, sea, modernCities, markets, adminCenters, fields, seed, minPopulation = 200000, focusedPrefectureMask = null) { if (!prefectureRegionId) return 0; const prefIds = new Set(); for (let i = 0; i < prefectureRegionId.length; i++) { const id = prefectureRegionId[i]; if (!sea[i] && id >= 0) prefIds.add(id); } const prefProfiles = new Map(); for (let i = 0; i < prefectureRegionId.length; i++) { const prefId = prefectureRegionId[i]; if (sea[i] || prefId < 0) continue; const profile = prefProfiles.get(prefId) || { landCells: 0, habitableCells: 0, lowlandCells: 0, densitySum: 0 }; profile.landCells++; const slopeV = fields.slope?.[i] || 0; const ridgeV = fields.ridgeField?.[i] || 0; const elevV = fields.elevation?.[i] || 0; const lowland = ((fields.plain?.[i] || 0) > 0.24 || (fields.basinField?.[i] || 0) > 0.26 || (fields.coastalLowland?.[i] || 0) > 0.22) && slopeV < 0.38 && ridgeV < 0.55; if (slopeV < 0.42 && ridgeV < 0.58 && elevV < 0.74) profile.habitableCells++; if (lowland) profile.lowlandCells++; profile.densitySum += fields.populationDensity?.[i] || 0; prefProfiles.set(prefId, profile); } function capitalFloorForPref(prefId) { const p = prefProfiles.get(prefId) || { landCells: 0, habitableCells: 0, lowlandCells: 0, densitySum: 0 }; const lowlandRatio = p.landCells ? p.lowlandCells / p.landCells : 0; const densityBoost = clamp((p.densitySum / Math.max(1, p.landCells) - 0.16) / 0.42); let base = 45000; if (p.lowlandCells > 900 || (p.lowlandCells > 650 && lowlandRatio > 0.28)) base = minPopulation * 0.90; else if (p.lowlandCells > 520) base = 160000; else if (p.lowlandCells > 260) base = 110000; else if (p.lowlandCells > 120 || p.habitableCells > 420) base = 70000; const adjusted = base + densityBoost * 50000; return Math.round(clamp(adjusted, 42000, minPopulation + 45000) / 1000) * 1000; } let promoted = 0; function prefAt(p) { if (!p || !inside(p.x, p.y)) return -1; return prefectureRegionId[indexOf(p.x, p.y)] ?? -1; } const focusedPrefCounts = new Map(); if (focusedPrefectureMask) { for (let i = 0; i < focusedPrefectureMask.length; i++) { if (!focusedPrefectureMask[i] || sea[i]) continue; const prefId = prefectureRegionId[i] ?? -1; if (prefId >= 0) focusedPrefCounts.set(prefId, (focusedPrefCounts.get(prefId) || 0) + 1); } } const focusedPrefId = focusedPrefCounts.size ? [...focusedPrefCounts.entries()].sort((a, b) => b[1] - a[1])[0][0] : 0; for (const city of modernCities || []) { if (city.isPrefecturalCapital) { city.isPrefecturalCapital = false; city.rank = city.isRegionalCapital ? "Regional Capital" : city.population >= 200000 ? "Regional City" : "Local City"; city.kind = city.rank; } } for (const prefId of [...prefIds].sort((a, b) => a - b)) { const cities = (modernCities || []).filter((p) => prefAt(p) === prefId); let target = cities.slice().sort((a, b) => ((b.isRegionalCapital ? 800000 : 0) + (b.population || 0)) - ((a.isRegionalCapital ? 800000 : 0) + (a.population || 0)) )[0]; if (!target) { const market = (markets || []).filter((p) => prefAt(p) === prefId).sort((a, b) => (b.population || 0) - (a.population || 0))[0]; if (market) { target = { ...market, kind: "Regional Capital", rank: "Regional Capital", promotedFromMarketTown: true, labelPriorityBase: 900, }; modernCities.push(target); } } if (!target) { const center = (adminCenters || []).filter((p) => prefAt(p) === prefId).sort((a, b) => (b.municipalityPopulation || 0) - (a.municipalityPopulation || 0))[0]; if (center) { target = { ...center, kind: "Regional Capital", rank: "Regional Capital", promotedFromMunicipalCenter: true, labelPriorityBase: 880, }; modernCities.push(target); } } if (!target) continue; const regionalFloor = capitalFloorForPref(prefId); const candidateCapacity = Number.isFinite(target.capacity) ? Math.max(42000, target.capacity * 1.18) : Infinity; const randomizedFloor = Math.round((regionalFloor + rand(seed + 52000, prefId * 37 + 11) * Math.max(12000, regionalFloor * 0.28)) / 1000) * 1000; const promotedPopulation = Math.max(42000, Math.round(Math.min(randomizedFloor, candidateCapacity) / 1000) * 1000); if ((target.population || 0) < promotedPopulation) { target.population = promotedPopulation; promoted++; } target.isRegionalCapital = true; target.isPrefecturalCapital = prefId === focusedPrefId; target.rank = target.isPrefecturalCapital ? "Prefectural Capital" : "Regional Capital"; target.kind = target.rank; target.labelPriorityBase = Math.max(target.labelPriorityBase || 0, 1150); target.urbanRadius = clamp(6.2 + Math.sqrt(target.population) / 80, 9, 30); target.coreRadius = clamp(2.0 + Math.sqrt(target.population) / 390, 2.4, 6.4); target.sprawlRadius = clamp(target.urbanRadius * 1.34, target.urbanRadius + 2, 38); target.urbanWeight = clamp(1.05 + Math.log10(Math.max(10000, target.population)) * 0.30, 1.25, 2.55); } return promoted; } function buildPrefectureRegions(prefectureRegionId, sea, fields, seed, usedNames, nameDebug, options = {}) { if (!prefectureRegionId) return []; const { modernCities = [], adminCenters = [] } = options; const byId = new Map(); for (let i = 0; i < prefectureRegionId.length; i++) { const id = prefectureRegionId[i]; if (sea[i] || id < 0) continue; if (!byId.has(id)) byId.set(id, { id, area: 0, sx: 0, sy: 0, cells: [] }); const row = byId.get(id); const x = i % MAP_W; const y = Math.floor(i / MAP_W); row.area++; row.sx += x; row.sy += y; row.cells.push(i); } const regions = []; const capitalNameByPref = new Map(); for (const city of modernCities || []) { if (!city || !inside(city.x, city.y) || !city.name) continue; const prefId = prefectureRegionId[indexOf(city.x, city.y)]; if (prefId < 0) continue; const current = capitalNameByPref.get(prefId); const tier = city.isPrefecturalCapital || city.rank === "Prefectural Capital" || city.kind === "Prefectural Capital" ? 3 : city.isRegionalCapital || city.rank === "Regional Capital" || city.kind === "Regional Capital" ? 2 : 1; const score = tier * 50_000_000 + (city.population || 0); if (!current || score > current.score) capitalNameByPref.set(prefId, { name: stripMunicipalSuffix(city.name), score, x: city.x, y: city.y, population: city.population || 0, source: tier === 3 ? "prefecture-capital" : "city" }); } for (const center of adminCenters || []) { if (!center || !inside(center.x, center.y) || !center.name) continue; const prefId = prefectureRegionId[indexOf(center.x, center.y)]; if (prefId < 0 || capitalNameByPref.has(prefId)) continue; capitalNameByPref.set(prefId, { name: stripMunicipalSuffix(center.name), score: center.municipalityPopulation || 0, x: center.x, y: center.y, population: center.municipalityPopulation || 0, source: "admin" }); } for (const row of [...byId.values()].sort((a, b) => a.id - b.id)) { const cx = row.sx / Math.max(1, row.area); const cy = row.sy / Math.max(1, row.area); let bestI = row.cells[0]; let bestScore = -INF; for (const i of row.cells) { const x = i % MAP_W; const y = Math.floor(i / MAP_W); const score = (fields.populationDensity?.[i] || 0) * 1.6 + (fields.plain?.[i] || 0) * 0.28 + (fields.basinField?.[i] || 0) * 0.22 + (fields.coastalLowland?.[i] || 0) * 0.16 - (fields.slope?.[i] || 0) * 0.36 - (fields.ridgeField?.[i] || 0) * 0.30 - Math.hypot(x - cx, y - cy) * 0.08; if (score > bestScore) { bestScore = score; bestI = i; } } let x = bestI % MAP_W; let y = Math.floor(bestI / MAP_W); const capitalInfo = capitalNameByPref.get(row.id); if (capitalInfo && inside(capitalInfo.x, capitalInfo.y)) { const targetRing = clamp(5.5 + Math.sqrt(row.area) / 52, 6, 15); let labelI = bestI; let labelScore = -INF; for (const i of row.cells) { const lx = i % MAP_W; const ly = Math.floor(i / MAP_W); const d = Math.hypot(lx - capitalInfo.x, ly - capitalInfo.y); if (d < 2 || d > Math.max(19, targetRing * 2.2)) continue; const lu = fields.landuse?.[i] ?? 0; const builtPenalty = lu === 3 ? 1.2 : lu === 2 || lu === 4 || lu === 7 || lu === 8 ? 0.70 : 0; const score = -Math.abs(d - targetRing) * 0.38 - (fields.populationDensity?.[i] || 0) * 1.1 - builtPenalty + (fields.plain?.[i] || 0) * 0.22 + (fields.basinField?.[i] || 0) * 0.14 + (fields.coastalLowland?.[i] || 0) * 0.10 - (fields.slope?.[i] || 0) * 0.30 - (fields.ridgeField?.[i] || 0) * 0.24; if (score > labelScore) { labelScore = score; labelI = i; } } x = labelI % MAP_W; y = Math.floor(labelI / MAP_W); } regions.push({ id: row.id, x, y, area: row.area, kind: row.id === 0 ? "Current Prefecture" : "Prefecture", labelPriorityBase: 950 + Math.sqrt(row.area), forceLabel: true, capitalX: capitalInfo?.x, capitalY: capitalInfo?.y }); } const named = attachIdsAndNames(regions, "prefecture", seed + 41000, null, fields, usedNames, nameDebug) .map((region, index) => ({ ...region, featureId: region.id, id: regions[index].id, labelName: region.name })); const usedPrefNames = new Set(); for (const region of named) { const capital = capitalNameByPref.get(region.id)?.name; let candidate = capital && Array.from(capital).length >= 2 ? capital : stripMunicipalSuffix(region.name); if (!candidate) candidate = stripMunicipalSuffix(region.name) || `県域${region.id + 1}`; if (usedPrefNames.has(candidate)) candidate = `${candidate}${region.id + 1}`; candidate = `${String(candidate).replace(/[都道府県]$/u, "")}県`; region.name = candidate; region.labelName = candidate; region.prefectureCapitalDerivedName = Boolean(capital); usedPrefNames.add(candidate); } return named; } export function finishMapOutput({ seed, options, terrain, features, admin, geography = null, }) { const { terrainTemplate, seaLevel, elevation, moisture, slope, sea, ocean, lake, river, floodplain, plain, agriculture, ridgeField, valleyField, visibleRavineField, surfaceTextureField, basinField, coastalLowland, flowAccum, watershedId, erosionField, depositionField, arcSpineField, branchRidgeField, depositionalLowland, alluvialFanField, deltaField, naturalBarrierScore, riverPaths, mainRivers, tributaryRivers, smallStreams, prefectureMask, prefectureBorder, terrainDebug, } = terrain; const { cityPopulationCap, stationInfluence, roadInfluence, railInfluence2, settlementCluster, villages: inputVillages, geographicUrbanAnchors: inputGeographicUrbanAnchors = [], ports: inputPorts, crossings: inputCrossings, passes: inputPasses, markets: inputMarkets, castles: inputCastles, castleTowns: inputCastleTowns, premodernRoads, minorRoads, modernCities: inputModernCities, populationDensity, railways, branchRailways, ringRailways, externalRailways, stations: inputStations, industrialZones: inputIndustrialZones, nationalRoads, ringRoads, expressways, ringExpressways, icAccessRoads, externalRoads, externalExpressways, interchanges: inputInterchanges, logisticsParks: inputLogisticsParks, satelliteCities: inputSatelliteCities, newTowns: inputNewTowns, landuse, externalGateways: inputExternalGateways, transportDebug, } = features; const { adminCentersRaw, adminId, adminBorders, adminDebug, prefectureRegionId, municipalityToPrefectureId, regionalPrefectureBorders: adminRegionalPrefectureBorders, regionalDebug, naturalCompartmentId, naturalCompartments, } = admin; let villages = inputVillages; let geographicUrbanAnchors = inputGeographicUrbanAnchors; let ports = inputPorts; let crossings = inputCrossings; let passes = inputPasses; let markets = inputMarkets; let castles = inputCastles; let castleTowns = inputCastleTowns; let modernCities = inputModernCities; let stations = inputStations; let industrialZones = inputIndustrialZones; let interchanges = inputInterchanges; let logisticsParks = inputLogisticsParks; let satelliteCities = inputSatelliteCities; let newTowns = inputNewTowns; let externalGateways = inputExternalGateways; const outputProgress = (step) => options?.onProgress?.({ status: "output-step", key: "output", label: `Output: ${step}`, step }); const patchMode = options?.patchMode === true || options?.generationContext?.hasBoundaryWorld === true; outputProgress("final packaging"); // Use all generated prefecture regions for human-geography masks, not only // the focused prefecture. Population density itself is already generated in // the human stage and is not rebuilt here. const humanRegionMask = new Uint8Array(MAP_W * MAP_H); for (let i = 0; i < humanRegionMask.length; i++) { humanRegionMask[i] = !sea[i] && ((prefectureRegionId?.[i] ?? -1) >= 0 || prefectureMask[i]) ? 1 : 0; } // Population density is generated directly in the human stage. Do not rebuild // it here from land-use or municipal offices; output should only package and // name features. for (const city of modernCities) { const cap = cityPopulationCap(city); let targetCap = cap; if (patchMode) { // Patch candidates should not regularly introduce a new top-center-scale // metropolis. Existing cities in the world are preserved by mapPatch; this // only affects newly generated candidate cities before they are merged. const patchCap = city.isPrefecturalCapital ? 820000 : city.isRegionalCapital ? 680000 : 540000; targetCap = Math.min(targetCap, patchCap); } if (targetCap < INF && (city.population || 0) > targetCap) { city.population = Math.round(targetCap / 1000) * 1000; city.urbanRadius = clamp(5.0 + Math.sqrt(city.population) / 100, 6, patchMode ? 14 : 16); city.coreRadius = clamp(1.8 + Math.sqrt(city.population) / 400, 2.2, patchMode ? 4.8 : 5.2); city.urbanWeight = clamp(0.72 + Math.log10(Math.max(10000, city.population)) * 0.30, 1.0, patchMode ? 1.86 : 2.0); } } let castleRuins = castles.filter((_, i) => i % 2 === 1).map((c) => ({ ...c, kind: "Castle Ruins" })); const nameFields = { elevation, slope, sea, river, plain, agriculture, ridgeField, valleyField, basinField, coastalLowland, flowAccum, landuse, populationDensity }; const usedNames = new Set(); const nameDebug = createNameDebug(); outputProgress("feature naming"); villages = attachIdsAndNames(tagInsidePrefecture(villages, prefectureMask), "village", seed, null, nameFields, usedNames, nameDebug); geographicUrbanAnchors = attachIdsAndNames(tagInsidePrefecture(geographicUrbanAnchors, prefectureMask), "geoAnchor", seed, null, nameFields, usedNames, nameDebug); ports = attachIdsAndNames(tagInsidePrefecture(ports, prefectureMask), "port", seed, null, nameFields, usedNames, nameDebug); crossings = attachIdsAndNames(tagInsidePrefecture(crossings, prefectureMask), "crossing", seed, null, nameFields, usedNames, nameDebug); passes = attachIdsAndNames(tagInsidePrefecture(passes, prefectureMask), "pass", seed, null, nameFields, usedNames, nameDebug); markets = attachIdsAndNames(tagInsidePrefecture(markets, prefectureMask), "market", seed, null, nameFields, usedNames, nameDebug); castles = attachIdsAndNames(tagInsidePrefecture(castles, prefectureMask), "castle", seed, null, nameFields, usedNames, nameDebug); castleTowns = attachIdsAndNames(tagInsidePrefecture(castleTowns, prefectureMask), "castleTown", seed, null, nameFields, usedNames, nameDebug); modernCities = attachIdsAndNames(tagInsidePrefecture(modernCities, prefectureMask), "city", seed, null, nameFields, usedNames, nameDebug); stations = attachIdsAndNames(tagInsidePrefecture(stations, prefectureMask), "station", seed, null, nameFields, usedNames, nameDebug); industrialZones = attachIdsAndNames(tagInsidePrefecture(industrialZones, prefectureMask), "industrial", seed, null, nameFields, usedNames, nameDebug); interchanges = attachIdsAndNames(tagInsidePrefecture(interchanges, prefectureMask), "interchange", seed, null, nameFields, usedNames, nameDebug); logisticsParks = attachIdsAndNames(tagInsidePrefecture(logisticsParks, prefectureMask), "logistics", seed, null, nameFields, usedNames, nameDebug); satelliteCities = attachIdsAndNames(tagInsidePrefecture(satelliteCities, prefectureMask), "satellite", seed, null, nameFields, usedNames, nameDebug); newTowns = attachIdsAndNames(tagInsidePrefecture(newTowns, prefectureMask), "newtown", seed, null, nameFields, usedNames, nameDebug); castleRuins = attachIdsAndNames(tagInsidePrefecture(castleRuins, prefectureMask), "castleRuin", seed, null, nameFields, usedNames, nameDebug); externalGateways = attachIdsAndNames(tagInsidePrefecture(externalGateways, prefectureMask), "gateway", seed, "External Gateway", nameFields, usedNames, nameDebug); outputProgress("municipality naming"); const municipalCoherence = reconcileMunicipalMetadata({ adminId, prefectureRegionId, sea, adminCenters: adminCentersRaw, municipalityToPrefectureId, fields: nameFields, width: MAP_W, height: MAP_H, seed, }); if (adminDebug) adminDebug.municipalCoherence = municipalCoherence.debug; const coherentMunicipalityToPrefectureId = municipalCoherence.municipalityToPrefectureId || municipalityToPrefectureId; const adminCenters = attachIdsAndNames(tagInsidePrefecture(municipalCoherence.adminCenters, humanRegionMask), "admin", seed, "Municipal Center", nameFields, usedNames, nameDebug); const representativeFeatures = [ ...modernCities.map((p) => ({ ...p, representativeWeight: 5.0 + (p.population || 0) / 180000 })), ...markets.map((p) => ({ ...p, representativeWeight: 3.2 })), ...ports.map((p) => ({ ...p, representativeWeight: p.portClass === "major" ? 3.8 : 2.4 })), ...villages.map((p) => ({ ...p, representativeWeight: 1.6 })), ].filter((p) => p.name && (p.insidePrefecture || humanRegionMask[indexOf(p.x, p.y)])); for (const center of adminCenters) { const centerAdmin = adminId?.[indexOf(center.x, center.y)]; let best = null; let bestScore = -INF; for (const sameAdminOnly of [true, false]) { for (const feature of representativeFeatures) { const featureAdmin = adminId?.[indexOf(feature.x, feature.y)]; if (sameAdminOnly && centerAdmin >= 0 && featureAdmin >= 0 && featureAdmin !== centerAdmin) continue; const d = Math.hypot(center.x - feature.x, center.y - feature.y); const score = feature.representativeWeight - d * 0.11 - (sameAdminOnly ? 0 : 1.2); if (score > bestScore) { bestScore = score; best = feature; } } if (best) break; } center.generatedMunicipalityName = center.generatedMunicipalityName || center.name; if (best) { center.representativeFeatureId = best.id; center.representativeFeatureName = best.name; center.canonicalSettlementId = best.id; center.canonicalSettlementName = best.name; center.municipalityRootName = best.name; const bestAdmin = adminId?.[indexOf(best.x, best.y)]; const canSnapOffice = inside(best.x, best.y) && !sea[indexOf(best.x, best.y)] && ( centerAdmin == null || centerAdmin < 0 || bestAdmin == null || bestAdmin < 0 || bestAdmin === centerAdmin ); if (canSnapOffice) { center.generatedOfficeX = center.generatedOfficeX ?? center.x; center.generatedOfficeY = center.generatedOfficeY ?? center.y; center.x = best.x; center.y = best.y; center.officeSnappedToSettlement = true; } } else { center.municipalityRootName = center.generatedMunicipalityName; } } const usedAdminNames = new Set(); for (const [index, center] of adminCenters.entries()) { const municipalId = centerMunicipalityId(center, index); center.adminId = municipalId; center.adminNumericId = municipalId; center.municipalityId = municipalId; let candidate = municipalityNameFromRoot(center.municipalityRootName || center.canonicalSettlementName || center.generatedMunicipalityName || center.name, center, nameFields, seed, municipalId); const generated = municipalityNameFromRoot(center.generatedMunicipalityName || center.name, center, nameFields, seed + 177, municipalId); if (usedAdminNames.has(candidate) && Array.from(generated).length >= 2 && !usedAdminNames.has(generated)) { candidate = generated; } if (usedAdminNames.has(candidate)) { const suffix = municipalitySuffixForCenter(center, nameFields, seed + 313, municipalId); const rootSource = String(center.generatedMunicipalityName || center.name || center.municipalityRootName || `自治${municipalId + 1}`).replace(/[市町村区駅港城跡宿]$/gu, ""); const chars = Array.from(rootSource || "里郷"); const alternates = [ chars.slice(0, 2).join(""), chars.slice(-2).join(""), `${chars[0] || "里"}${["里", "郷", "野", "田", "川", "原", "浜", "浦"][(seed + municipalId) % 8]}`, `${["東", "西", "南", "北", "上", "下", "中"][(seed + municipalId) % 7]}${chars[0] || "里"}`, ].filter((v) => Array.from(v).length >= 2); for (let attempt = 0; attempt < alternates.length + 12 && usedAdminNames.has(candidate); attempt++) { const root = attempt < alternates.length ? alternates[attempt] : `第${(municipalId + attempt) % 10}`; candidate = `${root}${suffix}`; } } center.name = candidate; center.labelName = candidate; center.municipalityName = candidate; usedAdminNames.add(center.name); } const promotedPrefectureCapitals = promotePrefectureCapitalPopulations(prefectureRegionId, sea, modernCities, markets, adminCenters, nameFields, seed, 220000, prefectureMask); assignMunicipalityPopulations(adminCenters, adminId, nameFields, [ ...modernCities, ...markets, ...villages, ...satelliteCities, ...newTowns, ]); function addMunicipalCenterLocalAccess() { const debugLayers = transportDebug ? (transportDebug.layers || (transportDebug.layers = { repairedSegments: [], unservedSettlements: [] })) : { repairedSegments: [], unservedSettlements: [] }; debugLayers.repairedSegments ||= []; debugLayers.unservedSettlements ||= []; const influenceCache = new Map(); const signature = (paths) => `${paths?.length || 0}:${(paths || []).reduce((sum, path) => sum + (path?.length || 0), 0)}`; const cachedInfluence = (paths, radius, label) => { const key = `${label}:${radius}:${signature(paths)}`; let grid = influenceCache.get(key); if (!grid) { grid = influenceFromPaths(paths, radius); influenceCache.set(key, grid); } return grid; }; const accessInfluence = cachedInfluence([...nationalRoads, ...railways, ...externalRoads, ...minorRoads], 8, "municipal-access"); const localPenalty = cachedInfluence(minorRoads, 4, "municipal-minor"); const perPrefectureQuota = new Map(); const candidates = adminCenters .filter((p) => { if (!inside(p.x, p.y) || sea[indexOf(p.x, p.y)]) return false; const i = indexOf(p.x, p.y); const meaningful = (p.municipalityPopulation || 0) >= 4500 || (populationDensity?.[i] || 0) > 0.08 || (p.representativeFeatureName && accessInfluence[i] < 0.22); return meaningful && accessInfluence[i] < 0.42; }) .sort((a, b) => { const ai = indexOf(a.x, a.y); const bi = indexOf(b.x, b.y); const as = (a.municipalityPopulation || 0) + Math.max(0, 0.42 - accessInfluence[ai]) * 145000; const bs = (b.municipalityPopulation || 0) + Math.max(0, 0.42 - accessInfluence[bi]) * 145000; return bs - as; }) .filter((p) => { const prefId = coherentMunicipalityToPrefectureId?.[p.municipalityId] ?? -1; const used = perPrefectureQuota.get(prefId) || 0; if (used >= 18) return false; perPrefectureQuota.set(prefId, used + 1); return true; }) .slice(0, 95); function addLocalPenalty(path, radius = 4, strength = 0.20) { for (const [x, y] of path || []) { for (let dy = -radius; dy <= radius; dy++) { for (let dx = -radius; dx <= radius; dx++) { const nx = x + dx; const ny = y + dy; if (!inside(nx, ny)) continue; const d = Math.hypot(dx, dy); if (d > radius) continue; const i = indexOf(nx, ny); if (sea[i]) continue; localPenalty[i] = Math.max(localPenalty[i], strength * (1 - d / radius)); } } } } function routeAccess(start) { const startIndex = indexOf(start.x, start.y); if (sea[startIndex]) return []; const score = new Float32Array(MAP_W * MAP_H); const cameFrom = new Int32Array(MAP_W * MAP_H); const closed = new Uint8Array(MAP_W * MAP_H); score.fill(INF); cameFrom.fill(-1); const heap = new MinHeap(); score[startIndex] = 0; heap.push({ i: startIndex, f: 0 }); const maxExpanded = Math.min(MAP_W * MAP_H, 24000); let goal = -1; let expanded = 0; while (heap.length && expanded++ < maxExpanded) { const current = heap.pop(); if (!current || closed[current.i]) continue; closed[current.i] = 1; const [cx, cy] = xyOf(current.i); const straightDistance = Math.hypot(cx - start.x, cy - start.y); if (straightDistance > 3 && (accessInfluence[current.i] > 0.11 || localPenalty[current.i] > 0.06)) { goal = current.i; break; } if (straightDistance > 150) continue; for (let dy = -1; dy <= 1; dy++) { for (let dx = -1; dx <= 1; dx++) { if (!dx && !dy) continue; const nx = cx + dx; const ny = cy + dy; if (!inside(nx, ny)) continue; const ni = indexOf(nx, ny); if (closed[ni] || sea[ni]) continue; const step = Math.hypot(dx, dy); const highPenalty = Math.max(0, elevation[ni] - 0.64); const targetAttraction = Math.max(accessInfluence[ni] * 2.4, localPenalty[ni] * 1.25, roadInfluence[ni] * 1.8, railInfluence2[ni] * 1.2); const terrainCost = 1.0 + slope[ni] * 1.20 + ridgeField[ni] * 0.52 + highPenalty * 1.55 - plain[ni] * 0.38 - valleyField[ni] * 0.42 - coastalLowland[ni] * 0.18 + Math.max(0, localPenalty[ni] - 0.18) * 0.38 - targetAttraction; const nd = score[current.i] + Math.max(0.16, terrainCost) * step; if (nd < score[ni]) { score[ni] = nd; cameFrom[ni] = current.i; heap.push({ i: ni, f: nd }); } } } } if (goal < 0) return []; const path = []; for (let p = goal; p >= 0; p = cameFrom[p]) { path.push(xyOf(p)); if (p === startIndex) break; } path.reverse(); if (path.length < 4 || path.length > 112) return []; return routeQualityAcceptable(path, { sea, elevation, slope, potential: populationDensity, penalty: localPenalty, highElevationThreshold: 0.78, steepThreshold: 0.52 }, { minLength: 4, maxLength: 112, maxCompactness: 4.0, maxHighElevationShare: 0.22, maxSteepShare: 0.50, }) ? path : []; } for (const center of candidates) { const path = routeAccess(center); debugLayers.unservedSettlements.push({ x: center.x, y: center.y, kind: "Municipal Center", mode: "municipal-access", repaired: path.length >= 4 }); if (path.length < 4) continue; minorRoads.push(path); addLocalPenalty(path); debugLayers.repairedSegments.push({ mode: "municipal-local", path, from: "municipal-center", to: "network" }); } } addMunicipalCenterLocalAccess(); function pruneIsolatedFinalRoadComponents() { const debugLayers = transportDebug ? (transportDebug.layers || (transportDebug.layers = {})) : {}; const groups = [ ["minor", minorRoads], ["national", nationalRoads], ["external", externalRoads], ["expressway", expressways], ["externalExpressway", externalExpressways], ]; function rasterize(path, fn) { for (let k = 0; k < (path?.length || 0); k++) { const [x0, y0] = path[k]; const [x1, y1] = path[Math.min(k + 1, path.length - 1)]; const steps = Math.max(1, Math.ceil(Math.hypot(x1 - x0, y1 - y0))); for (let s = 0; s <= steps; s++) { const t = s / steps; const x = Math.round(x0 + (x1 - x0) * t); const y = Math.round(y0 + (y1 - y0) * t); fn(x, y); } } } function splitPathOnSea(path) { const chunks = []; let chunk = []; function pushPoint(x, y) { if (!inside(x, y) || sea[indexOf(x, y)]) { if (chunk.length >= 2) chunks.push(chunk); chunk = []; return; } if (!chunk.length || chunk[chunk.length - 1][0] !== x || chunk[chunk.length - 1][1] !== y) chunk.push([x, y]); } for (let k = 0; k < (path?.length || 0); k++) { const [x0, y0] = path[k]; const [x1, y1] = path[Math.min(k + 1, path.length - 1)]; const steps = Math.max(1, Math.ceil(Math.hypot(x1 - x0, y1 - y0))); for (let s = 0; s <= steps; s++) { const t = s / steps; pushPoint(Math.round(x0 + (x1 - x0) * t), Math.round(y0 + (y1 - y0) * t)); } } if (chunk.length >= 2) chunks.push(chunk); return chunks; } // Keep sea-crossing cells in the stored path so the renderer can draw // explicit bridge overlays. Connectivity analysis below ignores sea cells // when rasterizing components, so preserving them here does not make islands // falsely connected by ordinary land roads. function pathNearAdminCenter(path, radius = 0.75) { for (const center of adminCenters || []) { if (!center || !inside(center.x, center.y)) continue; for (const [x, y] of path || []) { if (Math.hypot(center.x - x, center.y - y) <= radius) return true; } } return false; } function pathNearRequiredExpresswayCity(path) { if (!path || path.length < 2) return false; for (const city of modernCities || []) { if (!city || (city.population || 0) < 100000 || !inside(city.x, city.y)) continue; const inner = Math.max(7, (city.coreRadius || 4) + 4.5); const outer = Math.max(inner + 7, (city.urbanRadius || 12) * 2.15); let inBand = false; let exits = false; for (const [x, y] of path) { const d = Math.hypot(city.x - x, city.y - y); if (d >= inner && d <= outer) inBand = true; if (d >= Math.max(22, (city.urbanRadius || 12) * 1.45)) exits = true; if (inBand && exits) return true; } } return false; } function components() { const occ = new Uint8Array(MAP_W * MAP_H); for (const [, paths] of groups) for (const path of paths || []) rasterize(path, (x, y) => { if (inside(x, y) && !sea[indexOf(x, y)]) occ[indexOf(x, y)] = 1; }); const seen = new Uint8Array(MAP_W * MAP_H); const out = []; for (let i = 0; i < occ.length; i++) { if (!occ[i] || seen[i]) continue; const queue = [i]; const cells = []; seen[i] = 1; for (let q = 0; q < queue.length; q++) { const cur = queue[q]; cells.push(cur); const [x, y] = xyOf(cur); for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) { if (!dx && !dy) continue; if (dx * dx + dy * dy > 5) continue; const nx = x + dx, ny = y + dy; if (!inside(nx, ny)) continue; const ni = indexOf(nx, ny); if (!occ[ni] || seen[ni]) continue; seen[ni] = 1; queue.push(ni); } } out.push({ cells, size: cells.length }); } return out.sort((a, b) => b.size - a.size); } function buildLandComponentIds() { const ids = new Int32Array(MAP_W * MAP_H); ids.fill(-1); let id = 0; const q = []; for (let i = 0; i < ids.length; i++) { if (ids[i] >= 0 || sea[i]) continue; ids[i] = id; q.length = 0; q.push(i); for (let h = 0; h < q.length; h++) { const cur = q[h]; const [x, y] = xyOf(cur); for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) { if (!dx && !dy) continue; const nx = x + dx, ny = y + dy; if (!inside(nx, ny)) continue; const ni = indexOf(nx, ny); if (sea[ni] || ids[ni] >= 0) continue; ids[ni] = id; q.push(ni); } } id++; } return ids; } function majorityLandId(cells, landIds) { const counts = new Map(); for (const i of cells || []) { const id = landIds[i]; if (id < 0) continue; counts.set(id, (counts.get(id) || 0) + 1); } let best = -1, bestN = 0; for (const [id, n] of counts) if (n > bestN) { best = id; bestN = n; } return best; } function componentNearAdminCenter(comp, radius = 3.2) { if (!comp?.cells?.length) return false; const mask = new Uint8Array(MAP_W * MAP_H); for (const ci of comp.cells) mask[ci] = 1; const r = Math.ceil(radius); for (const center of adminCenters || []) { if (!center || !inside(center.x, center.y)) continue; const cx = Math.round(center.x), cy = Math.round(center.y); for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) { if (dx * dx + dy * dy > radius * radius) continue; const x = cx + dx, y = cy + dy; if (inside(x, y) && mask[indexOf(x, y)]) return true; } } return false; } function routeIsolatedComponentToMain(comp, mainMask, mainCentroid, landIds, landIdValue) { if (!comp?.cells?.length || landIdValue < 0) return []; const dist = new Float64Array(MAP_W * MAP_H); dist.fill(INF); const prev = new Int32Array(MAP_W * MAP_H); prev.fill(-1); const heap = new MinHeap(); let seeded = 0; const stride = Math.max(1, Math.floor(comp.cells.length / 96)); for (let k = 0; k < comp.cells.length; k += stride) { const i = comp.cells[k]; if (sea[i] || landIds[i] !== landIdValue) continue; dist[i] = 0; prev[i] = i; const [x, y] = xyOf(i); heap.push({ i, f: Math.hypot(x - mainCentroid.x, y - mainCentroid.y) * 0.22 }); seeded++; } if (!seeded) return []; let goal = -1; let expanded = 0; const maxExpanded = 22000; while (heap.length && expanded < maxExpanded) { const current = heap.pop(); if (!current) break; const cur = current.i; expanded++; if (mainMask[cur] && dist[cur] > 2) { goal = cur; break; } const [x, y] = xyOf(cur); for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) { if (!dx && !dy) continue; const nx = x + dx, ny = y + dy; if (!inside(nx, ny)) continue; const ni = indexOf(nx, ny); if (sea[ni] || landIds[ni] !== landIdValue) continue; const step = Math.hypot(dx, dy); const terrainCost = 1.0 + (slope?.[ni] || 0) * 1.28 + (ridgeField?.[ni] || 0) * 0.66 + Math.max(0, (elevation?.[ni] || 0) - 0.66) * 1.75 - (valleyField?.[ni] || 0) * 0.42 - (plain?.[ni] || 0) * 0.18 - (coastalLowland?.[ni] || 0) * 0.08; const nd = dist[cur] + step * Math.max(0.38, terrainCost); if (nd >= dist[ni]) continue; dist[ni] = nd; prev[ni] = cur; const h = Math.hypot(nx - mainCentroid.x, ny - mainCentroid.y) * 0.22; heap.push({ i: ni, f: nd + h }); } } if (goal < 0) return []; const path = []; let cur = goal; for (let guard = 0; guard < 240 && cur >= 0; guard++) { const [x, y] = xyOf(cur); path.push([x, y]); if (prev[cur] === cur) break; cur = prev[cur]; } path.reverse(); if (path.length < 4 || path.length > 150) return []; return routeQualityAcceptable(path, { sea, elevation, slope, potential: populationDensity, highElevationThreshold: 0.80, steepThreshold: 0.55 }, { minLength: 4, maxLength: 150, maxCompactness: 5.4, maxHighElevationShare: 0.42, maxSteepShare: 0.66, }) ? path : []; } function attemptConnectSameLandmassAdminRoadComponents(comps) { const result = { attempted: 0, added: 0, skippedIsland: 0, failed: 0 }; if (!comps || comps.length <= 1) return result; const landIds = buildLandComponentIds(); const mainLand = majorityLandId(comps[0].cells, landIds); const mainMask = new Uint8Array(MAP_W * MAP_H); let sx = 0, sy = 0, sn = 0; for (const ci of comps[0].cells) { const [cx, cy] = xyOf(ci); sx += cx; sy += cy; sn++; for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) { if (dx * dx + dy * dy > 5) continue; const nx = cx + dx, ny = cy + dy; if (inside(nx, ny)) mainMask[indexOf(nx, ny)] = 1; } } const centroid = { x: sx / Math.max(1, sn), y: sy / Math.max(1, sn) }; for (const comp of comps.slice(1, 24)) { if (!componentNearAdminCenter(comp)) continue; const land = majorityLandId(comp.cells, landIds); if (land !== mainLand) { result.skippedIsland++; continue; } result.attempted++; const path = routeIsolatedComponentToMain(comp, mainMask, centroid, landIds, land); if (path.length >= 4) { minorRoads.push(path); result.added++; } else { result.failed++; } } return result; } let comps = components(); const before = comps.length; const mountainConnect = attemptConnectSameLandmassAdminRoadComponents(comps); if (mountainConnect.added > 0) comps = components(); const pruned = { minor: 0, national: 0, external: 0, expressway: 0, externalExpressway: 0 }; for (let pass = 0; pass < 4 && comps.length > 1; pass++) { const mainMask = new Uint8Array(MAP_W * MAP_H); for (const ci of comps[0].cells) { const [cx, cy] = xyOf(ci); for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) { if (dx * dx + dy * dy > 5) continue; const nx = cx + dx, ny = cy + dy; if (inside(nx, ny)) mainMask[indexOf(nx, ny)] = 1; } } function touchesMain(path) { let hit = 0, n = 0; rasterize(path, (x, y) => { if (!inside(x, y) || sea[indexOf(x, y)]) return; n++; if (mainMask[indexOf(x, y)]) hit++; }); return n > 0 && hit / n >= (pass === 0 ? 0.10 : 0.01); } for (const [key, paths] of groups) { const kept = []; for (const path of paths || []) { if (touchesMain(path) || pathNearAdminCenter(path) || ((key === "expressway" || key === "externalExpressway") && pathNearRequiredExpresswayCity(path))) kept.push(path); else pruned[key]++; } paths.length = 0; paths.push(...kept); } comps = components(); } debugLayers.finalOutputRoadConnectivity = { beforeComponents: before, afterComponents: comps.length, pruned, mountainAdminConnections: mountainConnect }; } pruneIsolatedFinalRoadComponents(); function ensureAdminCenterCellsAfterOutputPrune() { let added = 0; function roadTouches(center) { for (const path of [...minorRoads, ...nationalRoads, ...externalRoads]) { for (const [x, y] of path || []) if (Math.hypot(center.x - x, center.y - y) <= 0.65) return true; } return false; } for (const center of adminCenters || []) { if (!center || !inside(center.x, center.y) || sea[indexOf(center.x, center.y)]) continue; if (roadTouches(center)) continue; const x = Math.round(center.x); const y = Math.round(center.y); const horizontal = [[Math.max(0, x - 1), y], [x, y], [Math.min(MAP_W - 1, x + 1), y]] .filter((pt, idx, arr) => idx === 0 || pt[0] !== arr[idx - 1][0] || pt[1] !== arr[idx - 1][1]); const vertical = [[x, Math.max(0, y - 1)], [x, y], [x, Math.min(MAP_H - 1, y + 1)]] .filter((pt, idx, arr) => idx === 0 || pt[0] !== arr[idx - 1][0] || pt[1] !== arr[idx - 1][1]); minorRoads.push(horizontal.length >= 2 ? horizontal : vertical); added++; } if (transportDebug) { transportDebug.layers ||= {}; transportDebug.layers.adminCenterFinalStubs = added; } } ensureAdminCenterCellsAfterOutputPrune(); function connectNearbyRoadEndpoints() { const debugLayers = transportDebug ? (transportDebug.layers || (transportDebug.layers = {})) : {}; const ordinaryGroups = [ { key: "minor", paths: minorRoads || [] }, { key: "national", paths: nationalRoads || [] }, { key: "external", paths: externalRoads || [] }, { key: "ring", paths: ringRoads || [] }, ]; const occ = new Uint8Array(MAP_W * MAP_H); function rasterize(path, fn) { for (let k = 1; k < (path?.length || 0); k++) { const [x0, y0] = path[k - 1]; const [x1, y1] = path[k]; const steps = Math.max(1, Math.ceil(Math.hypot(x1 - x0, y1 - y0))); for (let s = 0; s <= steps; s++) { const t = s / steps; const x = Math.round(x0 + (x1 - x0) * t); const y = Math.round(y0 + (y1 - y0) * t); if (inside(x, y) && !sea[indexOf(x, y)]) fn(x, y); } } } for (const group of ordinaryGroups) for (const path of group.paths || []) rasterize(path, (x, y) => { occ[indexOf(x, y)] = 1; }); const comp = new Int32Array(MAP_W * MAP_H); comp.fill(-1); let compId = 0; const queue = []; for (let i = 0; i < occ.length; i++) { if (!occ[i] || comp[i] >= 0) continue; comp[i] = compId; queue.length = 0; queue.push(i); for (let q = 0; q < queue.length; q++) { const cur = queue[q]; const [x, y] = xyOf(cur); for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) { if (!dx && !dy) continue; const nx = x + dx, ny = y + dy; if (!inside(nx, ny)) continue; const ni = indexOf(nx, ny); if (!occ[ni] || comp[ni] >= 0) continue; comp[ni] = compId; queue.push(ni); } } compId++; } function endpointComponent(x, y) { if (!inside(x, y) || sea[indexOf(x, y)]) return -1; const here = comp[indexOf(x, y)]; if (here >= 0) return here; for (let r = 1; r <= 2; r++) { for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) { const nx = x + dx, ny = y + dy; if (!inside(nx, ny) || sea[indexOf(nx, ny)]) continue; const id = comp[indexOf(nx, ny)]; if (id >= 0) return id; } } return -1; } function directConnector(a, b) { const steps = Math.max(1, Math.ceil(Math.hypot(a.x - b.x, a.y - b.y))); const path = []; for (let s = 0; s <= steps; s++) { const t = s / steps; const x = Math.round(a.x + (b.x - a.x) * t); const y = Math.round(a.y + (b.y - a.y) * t); if (!inside(x, y) || sea[indexOf(x, y)]) return []; if (!path.length || path[path.length - 1][0] !== x || path[path.length - 1][1] !== y) path.push([x, y]); } return path.length >= 2 ? path : []; } const endpoints = []; for (const group of ordinaryGroups) { for (let pathIdx = 0; pathIdx < (group.paths?.length || 0); pathIdx++) { const path = group.paths[pathIdx]; if (!path || path.length < 2) continue; for (const end of [0, 1]) { const raw = end === 0 ? path[0] : path[path.length - 1]; const x = Math.round(raw[0]), y = Math.round(raw[1]); if (!inside(x, y) || sea[indexOf(x, y)]) continue; const ci = indexOf(x, y); const ruralBias = Math.max(0, 0.42 - (populationDensity?.[ci] || 0)); endpoints.push({ group: group.key, pathIdx, end, x, y, comp: endpointComponent(x, y), ruralBias }); } } } const pairs = []; for (let i = 0; i < endpoints.length; i++) { const a = endpoints[i]; if (a.comp < 0) continue; for (let j = i + 1; j < endpoints.length; j++) { const b = endpoints[j]; if (b.comp < 0 || a.comp === b.comp) continue; if (a.group === b.group && a.pathIdx === b.pathIdx) continue; const d = Math.hypot(a.x - b.x, a.y - b.y); const limit = (a.ruralBias + b.ruralBias) > 0.38 ? 6.5 : 4.4; if (d < 1.1 || d > limit) continue; const path = directConnector(a, b); if (path.length < 2 || path.length > 9) continue; pairs.push({ a, b, d, path, score: d - (a.ruralBias + b.ruralBias) * 1.25 + (a.group === "minor" && b.group === "minor" ? 0.25 : 0) }); } } pairs.sort((a, b) => a.score - b.score || a.d - b.d); const used = new Set(); let added = 0; for (const pair of pairs) { if (added >= 180) break; const ak = `${pair.a.group}:${pair.a.pathIdx}:${pair.a.end}`; const bk = `${pair.b.group}:${pair.b.pathIdx}:${pair.b.end}`; if (used.has(ak) || used.has(bk)) continue; minorRoads.push(pair.path); used.add(ak); used.add(bk); added++; } debugLayers.nearbyRoadEndpointConnectorsAdded = added; return added; } connectNearbyRoadEndpoints(); function renameInterchangesFromMunicipalities() { if (!interchanges?.length || !adminCenters?.length || !adminId) return 0; const centerByAdmin = new Map(); for (const center of adminCenters || []) { if (!center || !inside(center.x, center.y)) continue; const id = adminId[indexOf(center.x, center.y)]; if (id >= 0 && !centerByAdmin.has(id)) centerByAdmin.set(id, center); } const allCenters = [...centerByAdmin.values()].filter((c) => c?.name); const used = new Set(); const directionNames = ["北", "東", "南", "西", "中央", "上", "下", "新"]; let renamed = 0; function cleanBase(name) { return String(name || "").replace(/[ICインターチェンジ\s]+$/u, "").replace(/[市町村区]$/u, ""); } for (const [idx, ic] of interchanges.entries()) { if (!ic || !inside(ic.x, ic.y)) continue; const cell = indexOf(ic.x, ic.y); const admin = adminId[cell]; const primary = centerByAdmin.get(admin) || allCenters.slice().sort((a, b) => Math.hypot(a.x - ic.x, a.y - ic.y) - Math.hypot(b.x - ic.x, b.y - ic.y))[0]; const nearbyCenters = allCenters .slice() .sort((a, b) => Math.hypot(a.x - ic.x, a.y - ic.y) - Math.hypot(b.x - ic.x, b.y - ic.y)); const candidates = []; if (primary?.name) candidates.push(`${cleanBase(primary.municipalityName || primary.name)}IC`); for (const center of nearbyCenters.slice(0, 12)) { const base = cleanBase(center.municipalityName || center.name); if (base) candidates.push(`${base}IC`); } if (primary?.name) { const base = cleanBase(primary.municipalityName || primary.name); for (const dir of directionNames) candidates.push(`${base}${dir}IC`); } candidates.push(`自治${idx + 1}IC`); let name = candidates.find((candidate) => candidate && !used.has(candidate)); if (!name) name = `自治${idx + 1}IC`; ic.name = name; ic.labelName = name; ic.municipalityNameBased = true; used.add(name); renamed++; } if (transportDebug) { transportDebug.layers ||= {}; transportDebug.layers.municipalityBasedInterchangeNames = renamed; } return renamed; } renameInterchangesFromMunicipalities(); nameDebug.maxDerivedPerBase = 0; const prefectureRegions = buildPrefectureRegions(prefectureRegionId, sea, nameFields, seed, usedNames, nameDebug, { modernCities, adminCenters }); const regionalPrefectureBorders = adminRegionalPrefectureBorders || []; if (regionalDebug) { regionalDebug.finalRegionalPrefectureBorderCount = regionalPrefectureBorders.length; regionalDebug.regionalPrefectureBordersRebuiltFromFinalId = true; regionalDebug.promotedPrefectureCapitals = promotedPrefectureCapitals; } outputProgress("final package"); const entitiesForNames = [ ...modernCities, ...ports, ...markets, ...castles, ...stations, ...industrialZones, ...interchanges, ...logisticsParks, ...satelliteCities, ...newTowns, ...passes, ...crossings, ...adminCenters, ...externalGateways, ].filter((p) => p.insidePrefecture || p.kind === "External Gateway"); return applyOutputOptions({ width: MAP_W, height: MAP_H, originX: Number.isFinite(terrain?.originX) ? terrain.originX : (Number.isFinite(options?.originX) ? options.originX : 0), originY: Number.isFinite(terrain?.originY) ? terrain.originY : (Number.isFinite(options?.originY) ? options.originY : 0), generationContext: options?.generationContext || terrain?.generationContext || null, cellSize: CELL_SIZE, terrainTemplate, seaLevel, prefectureMask, humanRegionMask, prefectureBorder: regionalPrefectureBorders.length ? regionalPrefectureBorders : prefectureBorder, prefectureRegionId, municipalityToPrefectureId: coherentMunicipalityToPrefectureId, prefectureRegions, regionalDebug, terrainDebug, regionalPrefectureBorders, geography, geographyDebug: geography?.geographyDebug || null, habitability: geography?.habitability || null, accessibility: geography?.accessibility || null, centrality: geography?.centrality || null, naturalCentrality: geography?.naturalCentrality || null, humanCentrality: geography?.humanCentrality || null, transportAccessibility: geography?.transportAccessibility || null, geographicBarrier: geography?.geographicBarrier || null, geographicBarrierCost: geography?.geographicBarrierCost || geography?.barrierCost || null, barrierCost: geography?.barrierCost || geography?.geographicBarrierCost || null, corridorSuitability: geography?.corridorSuitability || null, adminBoundaryPreference: geography?.adminBoundaryPreference || null, boundaryAvoidance: geography?.boundaryAvoidance || null, lowlandCapacity: geography?.lowlandCapacity || null, valleyAccess: geography?.valleyAccess || null, coastalAccess: geography?.coastalAccess || null, geographicCompartmentProfiles: geography?.compartmentProfiles || [], watershedProfiles: geography?.watershedProfiles || [], elevation, moisture, slope, sea, ocean, lake, river, floodplain, plain, agriculture, settlementCluster, ridgeField, valleyField, visibleRavineField, surfaceTextureField, basinField, coastalLowland, flowAccum, watershedId, erosionField, depositionField, arcSpineField, branchRidgeField, depositionalLowland, alluvialFanField, deltaField, naturalBarrierScore, naturalCompartmentId, naturalCompartments, villages, geographicUrbanAnchors, ports, crossings, passes, markets, castles, castleTowns, premodernRoads, minorRoads, modernCities, prefecturalCapital: modernCities.find((city) => city.isPrefecturalCapital && prefectureMask[indexOf(city.x, city.y)]) || null, totalPopulation: [...modernCities, ...satelliteCities].reduce((sum, city) => sum + (city.population || 0), 0), populationDensity, railways, branchRailways, ringRailways, externalRailways, stations, industrialZones, nationalRoads, ringRoads, expressways, ringExpressways, icAccessRoads, externalRoads, externalExpressways, interchanges, logisticsParks, satelliteCities, newTowns, landuse, adminCenters, adminId, adminBorders, adminDebug, castleRuins, riverPaths, mainRivers, tributaryRivers, smallStreams, externalGateways, transportDebug, entitiesForNames, nameDebug, }, options); }