import { applyLandscapeUnitAdminPartition, assignAdminRegionsFromNaturalCompartments, lockSmallUrbanComponentsToMunicipality, mergeTinyMunicipalities, removeMunicipalExclaves, enforceMunicipalityConnectivityStrict, smoothAdminRegionsTerrainAware, snapAdminBoundariesToTerrain, } from "./adminRegions.js"; import { INF, clamp, indexOf, inside, rand } from "./mapUtils.js"; import { extractAdminBorderSegments } from "./mapGeneratorHelpers.js"; import { changedCellsSince, municipalityAreaById, maskLandArea, isProtectedAdminSeed, buildSeedLifecycle, activeSeedIds, } from "./mapAdminShared.js"; import { compactWholeCompartmentMunicipalities, enforceCompartmentMunicipalityOwnership, enforceSimpleAdministrativeHierarchy, lockCompactUrbanAreasToDominantAdmin, repairAdminSingleOwnerEnclaves, splitOversizedCompartmentMunicipalities, } from "./mapAdminCompartmentRepair.js"; import { generatePrefecturesFromMunicipalities } from "./mapPrefectureStage.js"; import { absorbSeedCompartments, splitOversizedLowlandsWithPendingSeeds, promotePendingSeedsForMunicipalityCount, restoreSurvivedSeedsByCompartment, } from "./mapAdminSeedLifecycle.js"; import { computeTargetMunicipalityCount, buildLowlandAdminSeeds, } from "./mapAdminTargets.js"; import { classifySatelliteMunicipalities, expandSatelliteMunicipalityCatchment, enforceCityMunicipalityCatchments, } from "./mapAdminUrbanCatchments.js"; function generateAdminLayoutForMask({ seed, prefectureMask, sea, elevation, slope, river, ridgeField, naturalBarrierScore, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, settlementScore, populationDensity, stationInfluence, roadInfluence, railInfluence2, villageInfluence, landuse, modernCities, satelliteCities, newTowns, markets, villages, ports, stations, industrialZones, logisticsParks, naturalCompartmentId, naturalCompartments, geography = null, habitability = null, accessibility = null, centrality = null, geographicBarrier = null, geographicBarrierCost = null, adminBoundaryPreference = null, boundaryAvoidance = null, adminRegionMeta = {}, adminProgress = null, }) { const unifiedBoundaryPreference = adminBoundaryPreference || geography?.adminBoundaryPreference || null; const unifiedBoundaryAvoidance = boundaryAvoidance || geography?.boundaryAvoidance || null; const unifiedGeographicBarrier = geographicBarrier || geography?.geographicBarrier || null; const boundaryRidgeField = naturalBarrierScore ? Float32Array.from(ridgeField, (value, i) => clamp(value * 0.72 + naturalBarrierScore[i] * 0.46)) : ridgeField; adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "classify satellites" }); const satelliteClassificationDebug = classifySatelliteMunicipalities(satelliteCities, modernCities, prefectureMask, sea, landuse, populationDensity, roadInfluence, railInfluence2, boundaryRidgeField, river, flowAccum); const targetMunicipalityCount = computeTargetMunicipalityCount({ prefectureMask, sea, elevation, slope, ridgeField: boundaryRidgeField, coastalLowland, basinField, habitability: habitability || geography?.habitability, centrality: centrality || geography?.centrality, accessibility: accessibility || geography?.accessibility, geographicBarrier: unifiedGeographicBarrier, modernCities, markets, ports, satelliteCities, villages, adminRegionMeta }); const compartmentMultiplier = clamp(4.6 + rand(seed, 1320) * 2.4, 4.6, 7.0); const regionLandArea = adminRegionMeta.landArea || maskLandArea(prefectureMask, sea); const minCompartmentTarget = clamp(Math.round(Math.max(targetMunicipalityCount * 3.4, regionLandArea / 78)), 22, 150); const maxCompartmentTarget = clamp(Math.round(Math.max(targetMunicipalityCount * 6.0, regionLandArea / 36)), minCompartmentTarget, 320); let targetCompartmentCount = clamp(Math.round(targetMunicipalityCount * compartmentMultiplier), minCompartmentTarget, maxCompartmentTarget); let adminCentersRaw = buildLowlandAdminSeeds({ seed, targetMunicipalityCount, prefectureMask, sea, elevation, slope, ridgeField: boundaryRidgeField, plain, basinField, coastalLowland, settlementScore, populationDensity, roadInfluence, railInfluence2, stationInfluence, landuse, habitability: habitability || geography?.habitability, accessibility: accessibility || geography?.accessibility, centrality: centrality || geography?.centrality, boundaryAvoidance: unifiedBoundaryAvoidance, adminBoundaryPreference: unifiedBoundaryPreference, geographicBarrier: unifiedGeographicBarrier, modernCities, satelliteCities, markets, ports, newTowns, stations, }); if (adminCentersRaw.length < targetMunicipalityCount) targetCompartmentCount = Math.max(targetCompartmentCount, minCompartmentTarget); adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "natural compartments", targetMunicipalityCount, targetCompartmentCount, seedCount: adminCentersRaw.length }); const compartmentAssignment = assignAdminRegionsFromNaturalCompartments(prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, basinField, coastalLowland, flowAccum, plain, agriculture, populationDensity, landuse, adminCentersRaw, { seed, targetMunicipalityCount, targetCompartmentCount, maxNaturalCompartmentArea: Math.max(32, Math.round(maskLandArea(prefectureMask, sea) / Math.max(1, targetCompartmentCount) * 1.65)), naturalCompartmentId, naturalCompartments, naturalBarrierScore, geography, habitability: habitability || geography?.habitability, accessibility: accessibility || geography?.accessibility, centrality: centrality || geography?.centrality, boundaryAvoidance: unifiedBoundaryAvoidance, adminBoundaryPreference: unifiedBoundaryPreference, geographicBarrier: unifiedGeographicBarrier, progress: (step) => adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step }), }); const adminId = compartmentAssignment.adminId; if (naturalCompartmentId && naturalCompartments) { const oversizedCompartmentSplit = splitOversizedCompartmentMunicipalities(adminId, adminCentersRaw, compartmentAssignment.compartments, prefectureMask, sea, { elevation, slope, ridgeField: boundaryRidgeField, plain, agriculture, basinField, coastalLowland, populationDensity, }, seed + 21900); const changedAfterInitialCompartmentOwnership = enforceCompartmentMunicipalityOwnership(adminId, compartmentAssignment.compartmentId, compartmentAssignment.compartments, prefectureMask, sea); const hierarchyRepair = enforceSimpleAdministrativeHierarchy(adminId, compartmentAssignment.compartments, prefectureMask, sea, { minCompartmentsPerMunicipality: 2, maxUrbanClusterCells: 1600, modernCities, }); const changedAfterFinalCompartmentOwnership = enforceCompartmentMunicipalityOwnership(adminId, compartmentAssignment.compartmentId, compartmentAssignment.compartments, prefectureMask, sea); const compacted = compactWholeCompartmentMunicipalities(adminId, adminCentersRaw, prefectureMask, sea, { populationDensity, plain, slope }); const strictConnectivityChangedCells = enforceMunicipalityConnectivityStrict(compacted.adminId, prefectureMask, sea, compacted.adminCentersRaw, [...modernCities, ...(compacted.adminCentersRaw || [])], 8); const strictEnclaveRepairChangedCells = repairAdminSingleOwnerEnclaves(compacted.adminId, prefectureMask, sea, 4); const adminBorders = extractAdminBorderSegments(compacted.adminId, prefectureMask); const actualMunicipalityCount = new Set([...compacted.adminId].filter((id, i) => id >= 0 && prefectureMask[i] && !sea[i])).size; const naturalCompartmentCount = compartmentAssignment.debug?.naturalCompartmentCount || naturalCompartments.filter((unit) => unit && unit.area > 0).length; const adminDebug = { ...compartmentAssignment.debug, simpleHierarchyPrototype: true, administrativeHierarchySpec: "unified-geography->natural-compartments->living-sphere-municipalities->prefectures", unifiedGeographyAdministrativeBasis: true, naturalCompartmentsImmutable: true, municipalitiesAreCompartmentGroups: true, prefecturesAreMunicipalityGroups: true, cellLevelAdminSmoothingDisabled: true, sharedNaturalCompartmentLayer: true, skippedLegacyCellCleanupForHierarchy: true, targetMunicipalityCount, actualMunicipalityCount, finalMunicipalityCount: actualMunicipalityCount, changedAfterStrictMunicipalityConnectivity: strictConnectivityChangedCells, changedAfterStrictMunicipalityEnclaveRepair: strictEnclaveRepairChangedCells, candidateSeedCount: adminCentersRaw.length, municipalOfficePointCount: compacted.adminCentersRaw.length, seedCellRevivalCount: 0, survivedSeedCount: compacted.activeMunicipalityCount, pendingSeedCount: 0, absorbedSeedCount: Math.max(0, adminCentersRaw.length - compacted.activeMunicipalityCount), targetNaturalCompartmentCount: targetCompartmentCount, naturalCompartmentCount, compartmentCount: naturalCompartmentCount, changedAfterCompartmentAssignment: naturalCompartmentCount, changedAfterInitialCompartmentOwnership, changedAfterFinalCompartmentOwnership, changedAfterUrbanUnification: hierarchyRepair.changedAfterUrbanUnification, urbanComponentsUnified: hierarchyRepair.urbanComponentsUnified, changedAfterCityMetroMunicipalityUnification: hierarchyRepair.changedAfterCityMetroMunicipalityUnification, cityMetroMunicipalitiesUnified: hierarchyRepair.cityMetroMunicipalitiesUnified, changedAfterCompartmentConnectivity: hierarchyRepair.changedAfterCompartmentConnectivity, disconnectedCompartmentComponentsMerged: hierarchyRepair.disconnectedCompartmentComponentsMerged, changedAfterAdminEnclaveRepair: hierarchyRepair.changedAfterCompartmentEnclaveRepair, compartmentEnclaveComponentsMerged: hierarchyRepair.compartmentEnclaveComponentsMerged, changedAfterSingleCompartmentMunicipalityMerge: hierarchyRepair.changedAfterSingleCompartmentMunicipalityMerge, singleCompartmentMunicipalitiesMerged: hierarchyRepair.singleCompartmentMunicipalitiesMerged, remainingSingleCompartmentMunicipalities: hierarchyRepair.remainingSingleCompartmentMunicipalities, changedAfterPostMergeCompartmentOwnership: changedAfterFinalCompartmentOwnership, changedAfterOversizedCompartmentSplit: oversizedCompartmentSplit.changedCells, oversizedCompartmentMunicipalitiesSplit: oversizedCompartmentSplit.splitMunicipalities, oversizedCompartmentSplitAddedCenters: oversizedCompartmentSplit.addedCenters, oversizedCompartmentSplitMaxArea: oversizedCompartmentSplit.maxArea || 0, finalTinyMunicipalityCount: [...municipalityAreaById(compacted.adminId, prefectureMask, sea).values()].filter((area) => area > 0 && area < 8).length, compartmentBorders: compartmentAssignment.debug?.compartmentBorders || [], borderNaturalBarrierAverage: compartmentAssignment.debug?.finalBorderNaturalBarrierAverage || 0, voronoiLikeRate: compartmentAssignment.debug?.voronoiLikeRateAfter || 0, }; return { adminCentersRaw: compacted.adminCentersRaw, adminId: compacted.adminId, adminBorders, adminDebug, naturalCompartmentId: compartmentAssignment.compartmentId, naturalCompartments: compartmentAssignment.compartments, }; } let previousSnapshot = new Int16Array(adminId); const adminDebug = { changedAfterSmooth: 0, changedAfterUrbanLock: 0, changedAfterSmallUrbanLock: 0, changedAfterInitialMerge: 0, changedAfterInitialExclaveRemoval: 0, changedAfterLandscapePartition: 0, changedAfterSnap: 0, changedAfterOversizedRuralSplit: 0, changedAfterFinalExclaveRemoval: 0, changedAfterFinalMerge: 0, targetMunicipalityCount, actualMunicipalityCount: 0, municipalityCountReason: "unified habitability/accessibility, settlement hierarchy, coastline complexity, basin/lowland bonus, and natural-barrier adjustment", unifiedGeographyAdministrativeBasis: true, administrativeHierarchySpec: "unified-geography->natural-compartments->living-sphere-municipalities->prefectures", changedAfterCompartmentAssignment: compartmentAssignment.debug?.naturalCompartmentCount || 0, oversizedRuralSplits: 0, oversizedLowlandSplits: 0, ruralSplitsAccepted: 0, ruralSplitsRejected: 0, targetNaturalCompartmentCount: targetCompartmentCount, compartmentMultiplier, lowlandAdminSeedCount: adminCentersRaw.filter((p) => p.invisibleLowlandAdminSeed).length, lowlandAdminSeeds: adminCentersRaw.filter((p) => p.invisibleLowlandAdminSeed).map((p) => ({ x: p.x, y: p.y })), realAdminSeedCount: adminCentersRaw.filter((p) => !p.invisibleLowlandAdminSeed).length, highMountainAdminSeedCount: adminCentersRaw.filter((p) => { const i = indexOf(p.x, p.y); return elevation[i] > 0.70 || slope[i] > 0.52 || boundaryRidgeField[i] > 0.62; }).length, candidateSeedCount: adminCentersRaw.length, protectedSeedCount: adminCentersRaw.filter(isProtectedAdminSeed).length, survivedSeedCount: 0, pendingSeedCount: 0, absorbedSeedCount: 0, pendingSeedsUsedForLowlandSplit: 0, finalMunicipalityCount: 0, finalTinyMunicipalityCount: 0, seedCellRevivalCount: 0, satelliteMunicipalitiesCreated: adminCentersRaw.filter((p) => p.protectedSatellite).length, satelliteMunicipalitiesMerged: 0, satelliteMunicipalitiesExpanded: 0, satelliteMunicipalitiesTooSmall: 0, averageSatelliteMunicipalityArea: 0, minSatelliteMunicipalityArea: 0, satelliteMunicipalityAreaByNameOrIndex: {}, independentSatelliteMunicipalities: satelliteClassificationDebug.independent, attachedSatelliteDistricts: satelliteClassificationDebug.attached, satelliteMunicipalityStats: satelliteClassificationDebug, ...compartmentAssignment.debug, }; adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "seed lifecycle" }); const seedLifecycle = buildSeedLifecycle(adminCentersRaw, adminId, prefectureMask, sea, 35); const pendingSplitDebug = splitOversizedLowlandsWithPendingSeeds(adminId, prefectureMask, sea, { plain, agriculture, basinField, coastalLowland, valleyField, ridgeField: boundaryRidgeField, slope, elevation, }, compartmentAssignment.compartments, adminCentersRaw, seedLifecycle, [...(satelliteCities || []), ...newTowns, ...markets, ...villages, ...ports]); adminDebug.changedAfterPendingSeedLowlandSplit = pendingSplitDebug.changedCells; adminDebug.pendingSeedsUsedForLowlandSplit = pendingSplitDebug.pendingSeedsUsed; adminDebug.oversizedLowlandSplits += pendingSplitDebug.splitMunicipalities; const pendingPromotionDebug = promotePendingSeedsForMunicipalityCount(adminId, prefectureMask, sea, { plain, agriculture, basinField, coastalLowland, valleyField, ridgeField: boundaryRidgeField, slope, elevation, }, compartmentAssignment.compartments, adminCentersRaw, seedLifecycle, Math.min(24, targetMunicipalityCount)); adminDebug.changedAfterPendingSeedCountRepair = pendingPromotionDebug.changedCells; adminDebug.pendingSeedsPromotedForCount = pendingPromotionDebug.promotedSeeds; let areaAfterPendingSplit = municipalityAreaById(adminId, prefectureMask, sea); for (const seedState of seedLifecycle) { if (seedState.state !== "pending") continue; seedState.area = areaAfterPendingSplit.get(seedState.id) || 0; if (seedState.area >= 35) seedState.state = "survived"; else seedState.state = "absorbed"; } adminDebug.changedAfterAbsorbingSeeds = absorbSeedCompartments(adminId, compartmentAssignment.compartments, seedLifecycle); let activeAdminIds = activeSeedIds(seedLifecycle); function markChanged(field) { adminDebug[field] = changedCellsSince(previousSnapshot, adminId, prefectureMask, sea); previousSnapshot = new Int16Array(adminId); } adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "smooth boundaries" }); smoothAdminRegionsTerrainAware(adminId, prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, populationDensity, landuse, 2); markChanged("changedAfterSmooth"); function lockUrbanClusterToMunicipality(city, radius, allowSuburban = true) { if (!city || !prefectureMask[indexOf(city.x, city.y)]) return; let bestAdmin = -1; let bestD = INF; adminCentersRaw.forEach((center, id) => { if (!activeAdminIds.has(id)) return; const d = Math.hypot(center.x - city.x, center.y - city.y); if (d < bestD) { bestD = d; bestAdmin = id; } }); if (bestAdmin < 0) return; const r = Math.ceil(radius); for (let dy = -r; dy <= r; dy++) { for (let dx = -r; dx <= r; dx++) { const x = city.x + dx; const y = city.y + dy; if (!inside(x, y)) continue; const i = indexOf(x, y); if (!prefectureMask[i] || sea[i]) continue; const d = Math.hypot(dx, dy); if (d > radius) continue; const urban = landuse[i] === 2 || landuse[i] === 3 || (allowSuburban && (landuse[i] === 4 || landuse[i] === 7 || landuse[i] === 8)); if (urban || populationDensity[i] > 0.22) adminId[i] = bestAdmin; } } } for (const city of modernCities) { const radius = (city.population || 0) >= 500000 ? clamp(17 + Math.sqrt(city.population) / 120, 20, 38) : clamp(5 + Math.sqrt(city.population || 70000) / 210, 6, 11); lockUrbanClusterToMunicipality(city, radius, true); } for (const sat of satelliteCities || []) { if (!prefectureMask[indexOf(sat.x, sat.y)]) continue; let bestAdmin = -1; if (sat.municipalityClass === "independentSatelliteMunicipality") { let bestD = INF; adminCentersRaw.forEach((center, id) => { if (!activeAdminIds.has(id)) return; const d = Math.hypot(center.x - sat.x, center.y - sat.y); if (d < bestD) { bestD = d; bestAdmin = id; } }); } else if (inside(sat.parentX ?? -1, sat.parentY ?? -1)) { bestAdmin = adminId[indexOf(sat.parentX, sat.parentY)]; } if (bestAdmin < 0) continue; sat.parentAdminHint = bestAdmin; const changed = expandSatelliteMunicipalityCatchment(adminId, sat, bestAdmin, { prefectureMask, sea, elevation, slope, river, ridgeField: boundaryRidgeField, valleyField, basinField, coastalLowland, flowAccum, landuse, populationDensity, roadInfluence, railInfluence2, stationInfluence, modernCities, }); if (changed > 0 && sat.municipalityClass === "independentSatelliteMunicipality") adminDebug.satelliteMunicipalitiesExpanded++; } markChanged("changedAfterUrbanLock"); lockSmallUrbanComponentsToMunicipality(adminId, prefectureMask, sea, landuse, populationDensity, 520); lockSmallUrbanComponentsToMunicipality(adminId, prefectureMask, sea, landuse, populationDensity, 620); markChanged("changedAfterSmallUrbanLock"); const activeAdminCenters = () => adminCentersRaw.filter((_, id) => activeAdminIds.has(id)); mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, modernCities, 120, { satelliteCities, satelliteStats: adminDebug, satelliteMinArea: 100, protectedPoints: activeAdminCenters() }); markChanged("changedAfterInitialMerge"); adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "topology cleanup" }); removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, modernCities, 180); markChanged("changedAfterInitialExclaveRemoval"); // The initial compartment graph assignment is now the primary natural partition. // Re-running the older raw landscape-unit pass here collapses lowland seeds into a few broad owners. markChanged("changedAfterLandscapePartition"); adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "split oversized municipalities" }); // The older oversized-lowland pass rebuilds natural compartments a second time. // The current pipeline already performs pending-seed lowland splitting on the active // compartment graph above, so keep the full admin layout while avoiding the duplicate // high-cost recomputation. const oversizedSplitDebug = { changedCells: 0, splitMunicipalities: 0, rejectedMunicipalities: 0, skippedDuplicateCompartmentRebuild: true, skippedForVisibleFragment: false, }; adminDebug.changedAfterOversizedRuralSplit = oversizedSplitDebug.changedCells; adminDebug.oversizedRuralMunicipalitiesSplit = oversizedSplitDebug.splitMunicipalities; adminDebug.oversizedRuralSplits = oversizedSplitDebug.splitMunicipalities; adminDebug.oversizedLowlandSplits = oversizedSplitDebug.splitMunicipalities; adminDebug.ruralSplitsAccepted = oversizedSplitDebug.splitMunicipalities; adminDebug.ruralSplitsRejected = oversizedSplitDebug.rejectedMunicipalities || 0; adminDebug.oversizedSplitSkippedForVisibleFragment = Boolean(oversizedSplitDebug.skippedForVisibleFragment); adminDebug.oversizedSplitSkippedDuplicateCompartmentRebuild = Boolean(oversizedSplitDebug.skippedDuplicateCompartmentRebuild); previousSnapshot = new Int16Array(adminId); adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "terrain snap" }); snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, flowAccum, populationDensity, landuse, adminCentersRaw, [...modernCities, ...satelliteCities, ...ports, ...industrialZones, ...logisticsParks], 2); markChanged("changedAfterSnap"); removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...activeAdminCenters()], 360); markChanged("changedAfterFinalExclaveRemoval"); mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, modernCities, 80, { satelliteCities, satelliteStats: adminDebug, satelliteMinArea: 90, protectedPoints: activeAdminCenters() }); markChanged("changedAfterFinalMerge"); for (const sat of satelliteCities || []) { if (sat.municipalityClass !== "independentSatelliteMunicipality" || !inside(sat.x, sat.y)) continue; const targetAdmin = adminId[indexOf(sat.x, sat.y)]; if (targetAdmin < 0) continue; expandSatelliteMunicipalityCatchment(adminId, sat, targetAdmin, { prefectureMask, sea, elevation, slope, river, ridgeField: boundaryRidgeField, valleyField, basinField, coastalLowland, flowAccum, landuse, populationDensity, roadInfluence, railInfluence2, stationInfluence, modernCities, }); } const cityCatchmentDebug = enforceCityMunicipalityCatchments(adminId, modernCities, { prefectureMask, sea, elevation, slope, river, ridgeField: boundaryRidgeField, valleyField, basinField, coastalLowland, flowAccum, landuse, populationDensity, roadInfluence, railInfluence2, stationInfluence, }); adminDebug.changedAfterCityMunicipalityCatchment = cityCatchmentDebug.changed; adminDebug.protectedCityMunicipalityCount = cityCatchmentDebug.protectedCities; adminDebug.tooSmallCityMunicipalityCountBeforeRepair = cityCatchmentDebug.tooSmall; snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, boundaryRidgeField, valleyField, flowAccum, populationDensity, landuse, adminCentersRaw, [...modernCities, ...activeAdminCenters()], 2); removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...activeAdminCenters()], 260); const finalPromotionDebug = promotePendingSeedsForMunicipalityCount(adminId, prefectureMask, sea, { plain, agriculture, basinField, coastalLowland, valleyField, ridgeField: boundaryRidgeField, slope, elevation, }, compartmentAssignment.compartments, adminCentersRaw, seedLifecycle, Math.min(22, targetMunicipalityCount)); adminDebug.changedAfterFinalPendingSeedCountRepair = finalPromotionDebug.changedCells; adminDebug.pendingSeedsPromotedForCount += finalPromotionDebug.promotedSeeds; const restoredSeedDebug = restoreSurvivedSeedsByCompartment(adminId, prefectureMask, sea, compartmentAssignment.compartments, adminCentersRaw, seedLifecycle, Math.min(22, targetMunicipalityCount)); adminDebug.changedAfterSurvivedSeedCompartmentRestore = restoredSeedDebug.changedCells; adminDebug.survivedSeedsRestoredByCompartment = restoredSeedDebug.restoredSeeds; let finalAreaBySeed = municipalityAreaById(adminId, prefectureMask, sea); for (const seedState of seedLifecycle) { seedState.area = finalAreaBySeed.get(seedState.id) || 0; if (!seedState.protected && seedState.state === "pending" && seedState.area < 25) seedState.state = "absorbed"; } absorbSeedCompartments(adminId, compartmentAssignment.compartments, seedLifecycle); activeAdminIds = activeSeedIds(seedLifecycle); adminDebug.changedAfterFinalCompartmentOwnership = enforceCompartmentMunicipalityOwnership(adminId, compartmentAssignment.compartmentId, compartmentAssignment.compartments, prefectureMask, sea); removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...activeAdminCenters()], 220); adminDebug.changedAfterPostCompartmentExclaveRemoval = enforceCompartmentMunicipalityOwnership(adminId, compartmentAssignment.compartmentId, compartmentAssignment.compartments, prefectureMask, sea); adminDebug.changedAfterUrbanUnification = lockCompactUrbanAreasToDominantAdmin(adminId, prefectureMask, sea, landuse, populationDensity, 980); adminDebug.changedAfterAdminEnclaveRepair = repairAdminSingleOwnerEnclaves(adminId, prefectureMask, sea, 6); adminDebug.changedAfterFinalCompartmentOwnership += enforceCompartmentMunicipalityOwnership(adminId, compartmentAssignment.compartmentId, compartmentAssignment.compartments, prefectureMask, sea); adminDebug.changedAfterStrictMunicipalityConnectivity = enforceMunicipalityConnectivityStrict(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...activeAdminCenters()], 8); adminDebug.changedAfterStrictMunicipalityEnclaveRepair = repairAdminSingleOwnerEnclaves(adminId, prefectureMask, sea, 4); const areaById = municipalityAreaById(adminId, prefectureMask, sea); const satelliteAreas = []; (satelliteCities || []).forEach((sat, index) => { if (!inside(sat.x, sat.y) || !prefectureMask[indexOf(sat.x, sat.y)]) return; const id = adminId[indexOf(sat.x, sat.y)]; const area = areaById.get(id) || 0; const key = sat.name || `satellite-${index}`; adminDebug.satelliteMunicipalityAreaByNameOrIndex[key] = area; if (sat.municipalityClass === "independentSatelliteMunicipality" && (area < Math.max(120, sat.satelliteMinArea || 0) || ((sat.population || 0) >= 60000 && area < 150))) { sat.municipalityClass = "smallTownAttachedToRuralMunicipality"; adminDebug.satelliteMunicipalitiesTooSmall++; return; } if (sat.municipalityClass === "independentSatelliteMunicipality") { satelliteAreas.push(area); if (area < 80) adminDebug.satelliteMunicipalitiesTooSmall++; } }); adminDebug.averageSatelliteMunicipalityArea = satelliteAreas.length ? satelliteAreas.reduce((sum, value) => sum + value, 0) / satelliteAreas.length : 0; adminDebug.minSatelliteMunicipalityArea = satelliteAreas.length ? Math.min(...satelliteAreas) : 0; adminDebug.satelliteMunicipalitiesIndependent = satelliteAreas.length; const landscapeDebug = applyLandscapeUnitAdminPartition.lastDebug || {}; Object.assign(adminDebug, landscapeDebug); adminDebug.targetNaturalCompartmentCount = compartmentAssignment.debug?.targetNaturalCompartmentCount || targetCompartmentCount; adminDebug.naturalCompartmentCount = compartmentAssignment.debug?.naturalCompartmentCount || adminDebug.naturalCompartmentCount || adminDebug.compartmentCount || 0; adminDebug.compartmentCount = adminDebug.naturalCompartmentCount; adminDebug.averageCompartmentsPerMunicipality = compartmentAssignment.debug?.averageCompartmentsPerMunicipality || adminDebug.averageCompartmentsPerMunicipality || 0; adminDebug.singleCompartmentMunicipalityRatio = compartmentAssignment.debug?.singleCompartmentMunicipalityRatio ?? adminDebug.singleCompartmentMunicipalityRatio ?? 0; adminDebug.actualMunicipalityCount = new Set([...adminId].filter((id, i) => id >= 0 && prefectureMask[i] && !sea[i])).size; adminDebug.averageCompartmentsPerMunicipality = adminDebug.actualMunicipalityCount ? adminDebug.naturalCompartmentCount / adminDebug.actualMunicipalityCount : 0; adminDebug.survivedSeedCount = seedLifecycle.filter((seed) => seed.state === "survived").length; adminDebug.pendingSeedCount = seedLifecycle.filter((seed) => seed.state === "pending").length; adminDebug.absorbedSeedCount = seedLifecycle.filter((seed) => seed.state === "absorbed").length; adminDebug.finalMunicipalityCount = adminDebug.actualMunicipalityCount; adminDebug.finalTinyMunicipalityCount = [...areaById.values()].filter((area) => area > 0 && area < 8).length; adminDebug.seedLifecycle = seedLifecycle.map((seed) => ({ id: seed.id, state: seed.state, protected: seed.protected, area: seed.area })); adminDebug.borderNaturalBarrierAverage = adminDebug.finalBorderNaturalBarrierAverage ?? 0; adminDebug.voronoiLikeRate = adminDebug.voronoiLikeRateAfter ?? 0; adminProgress?.({ status: "admin-step", regionId: adminRegionMeta.regionId, step: "extract borders" }); const adminBorders = extractAdminBorderSegments(adminId, prefectureMask); return { adminCentersRaw, adminId, adminBorders, adminDebug, naturalCompartmentId: compartmentAssignment.compartmentId, naturalCompartments: compartmentAssignment.compartments, }; } export function generateAdminLayout(context) { const layout = generateAdminLayoutForMask(context); return { ...layout, ...generatePrefecturesFromMunicipalities(context, layout) }; }