2026-08-11 21:51:07 +09:00
import { INF , MAP _W , MAP _H , SIZE , indexOf , inside , rand , xyOf } from "./mapUtils.js" ;
2026-05-28 00:30:09 +09:00
import { pathLengthCells } from "./mapTransport.js" ;
2026-08-11 21:51:07 +09:00
import { createIncrementalPathInfluence , normalizeTransportPathSet , smoothRasterPath } from "./mapTransportUtils.js" ;
// Post-admin routing runs hundreds of short A* searches on the same raster.
// Reuse the large working buffers and keep the heap numeric so each search does
// not allocate/fill O(SIZE) typed arrays or thousands of {i,f} objects.
class ReusableIndexMinHeap {
constructor ( capacity = 8192 ) {
const n = Math . max ( 64 , Math . min ( Math . max ( 64 , SIZE ) , capacity ) ) ;
this . indices = new Int32Array ( n ) ;
this . priorities = new Float64Array ( n ) ;
this . length = 0 ;
}
reset ( ) { this . length = 0 ; }
ensureCapacity ( required ) {
if ( required <= this . indices . length ) return ;
let n = this . indices . length ;
while ( n < required ) n = Math . min ( Math . max ( required , n * 2 ) , Math . max ( required , SIZE * 4 ) ) ;
const nextIndices = new Int32Array ( n ) ; nextIndices . set ( this . indices ) ; this . indices = nextIndices ;
const nextPriorities = new Float64Array ( n ) ; nextPriorities . set ( this . priorities ) ; this . priorities = nextPriorities ;
}
push ( index , priority ) {
const end = this . length ++ ;
this . ensureCapacity ( this . length ) ;
let i = end ;
while ( i > 0 ) {
const parent = ( i - 1 ) >> 1 ;
if ( this . priorities [ parent ] <= priority ) break ;
this . indices [ i ] = this . indices [ parent ] ;
this . priorities [ i ] = this . priorities [ parent ] ;
i = parent ;
}
this . indices [ i ] = index ;
this . priorities [ i ] = priority ;
}
pop ( ) {
if ( this . length <= 0 ) return - 1 ;
const rootIndex = this . indices [ 0 ] ;
const lastPos = -- this . length ;
if ( lastPos > 0 ) {
const lastIndex = this . indices [ lastPos ] ;
const lastPriority = this . priorities [ lastPos ] ;
let i = 0 ;
while ( true ) {
const left = i * 2 + 1 ;
if ( left >= lastPos ) break ;
const right = left + 1 ;
const child = right < lastPos && this . priorities [ right ] < this . priorities [ left ] ? right : left ;
if ( this . priorities [ child ] >= lastPriority ) break ;
this . indices [ i ] = this . indices [ child ] ;
this . priorities [ i ] = this . priorities [ child ] ;
i = child ;
}
this . indices [ i ] = lastIndex ;
this . priorities [ i ] = lastPriority ;
}
return rootIndex ;
}
}
function createRoutingWorkspace ( DistanceArray ) {
return {
dist : new DistanceArray ( SIZE ) ,
prev : new Int32Array ( SIZE ) ,
distStamp : new Uint32Array ( SIZE ) ,
closedStamp : new Uint32Array ( SIZE ) ,
metricStamp : new Uint32Array ( SIZE ) ,
startMetric : new Float64Array ( SIZE ) ,
goalMetric : new Float64Array ( SIZE ) ,
generation : 0 ,
heap : new ReusableIndexMinHeap ( ) ,
} ;
}
const primaryRouteWorkspace = createRoutingWorkspace ( Float64Array ) ;
const fallbackRouteWorkspace = createRoutingWorkspace ( Float32Array ) ;
let failedPrimaryRouteKeys = new Set ( ) ;
let failedFallbackRouteKeys = new Set ( ) ;
function beginRoutingSearch ( workspace ) {
workspace . generation = ( workspace . generation + 1 ) >>> 0 ;
if ( workspace . generation === 0 ) {
workspace . distStamp . fill ( 0 ) ;
workspace . closedStamp . fill ( 0 ) ;
workspace . metricStamp . fill ( 0 ) ;
workspace . generation = 1 ;
}
workspace . heap . reset ( ) ;
return workspace . generation ;
}
function routeSearchBounds ( startX , startY , goalX , goalY , maxLength , snapRadius ) {
const direct = Math . hypot ( goalX - startX , goalY - startY ) ;
const sumLimit = maxLength + snapRadius ;
if ( sumLimit + 1e-7 < direct ) return null ;
const semiMajor = Math . max ( direct * 0.5 , sumLimit * 0.5 ) ;
const focal = direct * 0.5 ;
const semiMinor = Math . sqrt ( Math . max ( 0 , semiMajor * semiMajor - focal * focal ) ) ;
const ux = direct > 1e-9 ? ( goalX - startX ) / direct : 1 ;
const uy = direct > 1e-9 ? ( goalY - startY ) / direct : 0 ;
const extentX = Math . sqrt ( semiMajor * semiMajor * ux * ux + semiMinor * semiMinor * uy * uy ) ;
const extentY = Math . sqrt ( semiMajor * semiMajor * uy * uy + semiMinor * semiMinor * ux * ux ) ;
const cx = ( startX + goalX ) * 0.5 ;
const cy = ( startY + goalY ) * 0.5 ;
const minX = Math . max ( 0 , Math . floor ( cx - extentX ) - 1 ) ;
const maxX = Math . min ( MAP _W - 1 , Math . ceil ( cx + extentX ) + 1 ) ;
const minY = Math . max ( 0 , Math . floor ( cy - extentY ) - 1 ) ;
const maxY = Math . min ( MAP _H - 1 , Math . ceil ( cy + extentY ) + 1 ) ;
return { minX , maxX , minY , maxY , cellUpperBound : Math . max ( 1 , ( maxX - minX + 1 ) * ( maxY - minY + 1 ) ) , sumLimit } ;
}
function routeFailureKey ( start , goal , maxLength , maxExpanded , snapRadius , maxElevation , strictTerrain , maxSeaRun , maxTunnelRun ) {
return ` ${ start } : ${ goal } : ${ maxLength } : ${ maxExpanded } : ${ snapRadius } : ${ maxElevation } : ${ strictTerrain ? 1 : 0 } : ${ maxSeaRun ? ? "" } : ${ maxTunnelRun ? ? "" } ` ;
}
function pathMaxVertexGap ( path ) {
let maxGap = 0 ;
for ( let k = 1 ; k < ( path ? . length || 0 ) ; k ++ ) {
const a = path [ k - 1 ] , b = path [ k ] ;
if ( ! a || ! b ) continue ;
maxGap = Math . max ( maxGap , Math . hypot ( b [ 0 ] - a [ 0 ] , b [ 1 ] - a [ 1 ] ) ) ;
}
return maxGap ;
}
2026-05-28 00:30:09 +09:00
function pathTouchesCell ( path , x , y , radius = 0.65 ) {
if ( ! path || path . length < 1 ) return false ;
for ( const [ px , py ] of path ) if ( Math . hypot ( px - x , py - y ) <= radius ) return true ;
return false ;
}
function anyPathTouches ( paths , p , radius = 0.65 ) {
2026-08-11 21:51:07 +09:00
return Boolean ( nearestPointOnPaths ( paths , p , radius ) ) ;
2026-05-28 00:30:09 +09:00
}
function pathTerrainRuns ( path , terrain = null ) {
const sea = terrain ? . sea ;
const elevation = terrain ? . elevation ;
const ridgeField = terrain ? . ridgeField ;
const naturalBarrierScore = terrain ? . naturalBarrierScore ;
let seaRun = 0 , maxSeaRun = 0 , tunnelRun = 0 , maxTunnelRun = 0 ;
2026-05-28 15:48:42 +09:00
let sampled = 0 ;
const visit = ( x , y ) => {
if ( ! inside ( x , y ) ) {
seaRun ++ ;
maxSeaRun = Math . max ( maxSeaRun , seaRun ) ;
tunnelRun = 0 ;
sampled ++ ;
return ;
}
2026-05-28 00:30:09 +09:00
const i = indexOf ( x , y ) ;
const isSea = Boolean ( sea ? . [ i ] ) ;
2026-05-28 15:48:42 +09:00
// Use the same sensitive tunnel proxy as the main transport validator.
// Sampling every raster cell along each segment prevents smoothed or direct
// paths from hiding over-limit tunnel runs between sparse vertices.
2026-08-11 21:51:07 +09:00
const elevationV = elevation ? . [ i ] || 0 ;
const ridgeV = ridgeField ? . [ i ] || 0 ;
const barrierV = naturalBarrierScore ? . [ i ] || 0 ;
const isTunnel = ! isSea && elevationV >= 0.58
&& ( ( elevationV >= 0.69 && ridgeV >= 0.34 ) || ( ridgeV >= 0.62 && elevationV >= 0.60 ) || ( barrierV >= 0.82 && elevationV >= 0.60 ) ) ;
2026-05-28 00:30:09 +09:00
if ( isSea ) { seaRun ++ ; maxSeaRun = Math . max ( maxSeaRun , seaRun ) ; } else seaRun = 0 ;
if ( isTunnel ) { tunnelRun ++ ; maxTunnelRun = Math . max ( maxTunnelRun , tunnelRun ) ; } else tunnelRun = 0 ;
2026-05-28 15:48:42 +09:00
sampled ++ ;
} ;
for ( let k = 1 ; k < ( path ? . length || 0 ) ; k ++ ) {
const a = path [ k - 1 ] ;
const b = path [ k ] ;
if ( ! a || ! b ) continue ;
const steps = Math . max ( 1 , Math . ceil ( Math . hypot ( b [ 0 ] - a [ 0 ] , b [ 1 ] - a [ 1 ] ) ) ) ;
for ( let s = 0 ; s <= steps ; s ++ ) {
if ( k > 1 && s === 0 ) continue ;
const t = s / steps ;
visit ( Math . round ( a [ 0 ] + ( b [ 0 ] - a [ 0 ] ) * t ) , Math . round ( a [ 1 ] + ( b [ 1 ] - a [ 1 ] ) * t ) ) ;
}
2026-05-28 00:30:09 +09:00
}
2026-05-28 15:48:42 +09:00
if ( ( path ? . length || 0 ) === 1 ) visit ( path [ 0 ] [ 0 ] , path [ 0 ] [ 1 ] ) ;
return { maxSeaRun , maxTunnelRun , sampled } ;
2026-05-28 00:30:09 +09:00
}
2026-08-11 21:51:07 +09:00
function pathTerrainBurden ( path , terrain = null ) {
const elevation = terrain ? . elevation ;
const slope = terrain ? . slope ;
const ridgeField = terrain ? . ridgeField ;
const valleyField = terrain ? . valleyField ;
const plain = terrain ? . plain ;
const naturalBarrierScore = terrain ? . naturalBarrierScore ;
let samples = 0 ;
let slopeSum = 0 , ridgeSum = 0 , barrierSum = 0 , elevationSum = 0 , valleySum = 0 , plainSum = 0 , highBarrier = 0 ;
for ( let k = 1 ; k < ( path ? . length || 0 ) ; k ++ ) {
const a = path [ k - 1 ] ;
const b = path [ k ] ;
if ( ! a || ! b ) continue ;
const steps = Math . max ( 1 , Math . ceil ( Math . hypot ( b [ 0 ] - a [ 0 ] , b [ 1 ] - a [ 1 ] ) ) ) ;
for ( let s = 0 ; s <= steps ; s ++ ) {
if ( k > 1 && s === 0 ) continue ;
const t = s / steps ;
const x = Math . round ( a [ 0 ] + ( b [ 0 ] - a [ 0 ] ) * t ) ;
const y = Math . round ( a [ 1 ] + ( b [ 1 ] - a [ 1 ] ) * t ) ;
if ( ! inside ( x , y ) ) continue ;
const i = indexOf ( x , y ) ;
const slopeV = slope ? . [ i ] || 0 ;
const ridgeV = ridgeField ? . [ i ] || 0 ;
const barrierV = naturalBarrierScore ? . [ i ] || 0 ;
const elevationV = elevation ? . [ i ] || 0 ;
const valleyV = valleyField ? . [ i ] || 0 ;
const plainV = plain ? . [ i ] || 0 ;
samples ++ ;
slopeSum += slopeV ;
ridgeSum += ridgeV ;
barrierSum += barrierV ;
elevationSum += elevationV ;
valleySum += valleyV ;
plainSum += plainV ;
if ( ( barrierV >= 0.78 && elevationV >= 0.55 ) || ( elevationV >= 0.66 && ridgeV >= 0.32 ) || slopeV >= 0.52 ) highBarrier ++ ;
}
2026-05-28 00:30:09 +09:00
}
2026-08-11 21:51:07 +09:00
if ( ! samples ) return { samples : 0 , penalty : 0 , highBarrierShare : 0 } ;
const meanSlope = slopeSum / samples ;
const meanRidge = ridgeSum / samples ;
const meanBarrier = barrierSum / samples ;
const meanElevation = elevationSum / samples ;
const meanValley = valleySum / samples ;
const meanPlain = plainSum / samples ;
const highBarrierShare = highBarrier / samples ;
const penalty = meanSlope * 0.95 + meanRidge * 0.72 + meanBarrier * 1.10 + Math . max ( 0 , meanElevation - 0.66 ) * 1.25 + highBarrierShare * 1.45 - meanValley * 0.22 - meanPlain * 0.16 ;
return { samples , meanSlope , meanRidge , meanBarrier , meanElevation , meanValley , meanPlain , highBarrierShare , penalty } ;
2026-05-28 00:30:09 +09:00
}
2026-05-28 15:48:42 +09:00
function routeTerrainPath ( a , b , terrain = null , options = { } ) {
if ( ! a || ! b || ! inside ( a . x , a . y ) || ! inside ( b . x , b . y ) ) return [ ] ;
const sea = terrain ? . sea ;
const elevation = terrain ? . elevation ;
const slope = terrain ? . slope ;
const ridgeField = terrain ? . ridgeField ;
2026-08-11 21:51:07 +09:00
const valleyField = terrain ? . valleyField ;
const plain = terrain ? . plain ;
const naturalBarrierScore = terrain ? . naturalBarrierScore ;
const passSuitability = terrain ? . passSuitability ;
const sx = Math . round ( a . x ) , sy = Math . round ( a . y ) ;
const start = indexOf ( sx , sy ) ;
2026-05-28 15:48:42 +09:00
const goal = indexOf ( Math . round ( b . x ) , Math . round ( b . y ) ) ;
if ( sea ? . [ start ] || sea ? . [ goal ] ) return [ ] ;
const straight = Math . hypot ( a . x - b . x , a . y - b . y ) ;
const maxLength = options . maxLength ? ? straight * 2.8 + 60 ;
2026-08-11 21:51:07 +09:00
const snapRadius = options . snapRadius ? ? 2.0 ;
const bounds = routeSearchBounds ( sx , sy , b . x , b . y , maxLength , snapRadius ) ;
if ( ! bounds ) return [ ] ;
const configuredMaxExpanded = options . maxExpanded ? ? Math . max ( 9000 , Math . floor ( straight * straight * 5.5 ) ) ;
// maxExpanded now counts unique expanded cells, not stale duplicate heap pops.
// The ellipse bounding box is a mathematical upper bound for any path that
// could later pass the unchanged maxLength/snapRadius validator.
const maxExpanded = Math . min ( configuredMaxExpanded , bounds . cellUpperBound ) ;
// A low elevation ceiling is used by every trunk caller. Treat it as a
// request for strict terrain conformance even if an older call-site did not
// explicitly pass `strictTerrain`. This prevents the old subtly-curved
// Euclidean corridors that happened to remain just under the hard ceiling.
const requestedElevationCeiling = Number . isFinite ( options . maxElevation ) ? options . maxElevation : 0.695 ;
const strictTerrain = options . strictTerrain === true || requestedElevationCeiling <= 0.705 ;
const failureKey = routeFailureKey ( start , goal , maxLength , maxExpanded , snapRadius , requestedElevationCeiling , strictTerrain , options . maxSeaRun , options . maxTunnelRun ) ;
if ( failedPrimaryRouteKeys . has ( failureKey ) ) return [ ] ;
const workspace = primaryRouteWorkspace ;
const generation = beginRoutingSearch ( workspace ) ;
const { dist , prev , distStamp , closedStamp , metricStamp , startMetric , goalMetric , heap } = workspace ;
2026-05-28 15:48:42 +09:00
dist [ start ] = 0 ;
prev [ start ] = start ;
2026-08-11 21:51:07 +09:00
distStamp [ start ] = generation ;
metricStamp [ start ] = generation ;
startMetric [ start ] = 0 ;
goalMetric [ start ] = Math . hypot ( sx - b . x , sy - b . y ) ;
// Every legal step costs at least 0.40 per Euclidean cell because the edge
// cost is step * max(0.40, terrainCost). Subtract the accepted goal snap
// radius, making this a strong admissible/consistent A* lower bound.
const heuristicFloor = 0.40 ;
heap . push ( start , Math . max ( 0 , goalMetric [ start ] - snapRadius ) * heuristicFloor ) ;
2026-05-28 15:48:42 +09:00
let hit = - 1 ;
let expanded = 0 ;
2026-08-11 21:51:07 +09:00
while ( heap . length && expanded < maxExpanded ) {
const cur = heap . pop ( ) ;
if ( cur < 0 || closedStamp [ cur ] === generation ) continue ;
closedStamp [ cur ] = generation ;
expanded ++ ;
const x = cur % MAP _W ;
const y = ( cur / MAP _W ) | 0 ;
const goalDistance = metricStamp [ cur ] === generation ? goalMetric [ cur ] : Math . hypot ( x - b . x , y - b . y ) ;
if ( goalDistance <= snapRadius ) { hit = cur ; break ; }
2026-05-28 15:48:42 +09:00
if ( Math . hypot ( x - a . x , y - a . y ) > maxLength ) continue ;
2026-08-11 21:51:07 +09:00
const currentElevation = elevation ? . [ cur ] || 0 ;
const currentDist = dist [ cur ] ;
2026-05-28 15:48:42 +09:00
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 ;
2026-08-11 21:51:07 +09:00
if ( nx < bounds . minX || nx > bounds . maxX || ny < bounds . minY || ny > bounds . maxY ) continue ;
const ni = ny * MAP _W + nx ;
if ( closedStamp [ ni ] === generation || sea ? . [ ni ] ) continue ;
let nextStartDistance , nextGoalDistance ;
if ( metricStamp [ ni ] === generation ) {
nextStartDistance = startMetric [ ni ] ;
nextGoalDistance = goalMetric [ ni ] ;
} else {
nextStartDistance = Math . hypot ( nx - sx , ny - sy ) ;
nextGoalDistance = Math . hypot ( nx - b . x , ny - b . y ) ;
startMetric [ ni ] = nextStartDistance ;
goalMetric [ ni ] = nextGoalDistance ;
metricStamp [ ni ] = generation ;
}
// Safe ROI: any finally accepted path must satisfy
// d(start,p)+d(p,goal) <= maxLength+snapRadius at every visited cell.
if ( nextStartDistance + nextGoalDistance > bounds . sumLimit + 1e-7 ) continue ;
const elev = elevation ? . [ ni ] || 0 ;
const slopeV = slope ? . [ ni ] || 0 ;
const ridgeV = ridgeField ? . [ ni ] || 0 ;
const barrierV = naturalBarrierScore ? . [ ni ] || 0 ;
const passV = passSuitability ? . [ ni ] || 0 ;
const elevationCeiling = requestedElevationCeiling ;
if ( strictTerrain ) {
if ( elev >= elevationCeiling ) continue ;
if ( elev >= 0.66 && passV < 0.50 ) continue ;
} else if ( elev >= elevationCeiling ) continue ;
const directionalGrade = Math . abs ( elev - currentElevation ) ;
const hardBarrier = strictTerrain
? ( slopeV >= 0.52 || ( ridgeV >= 0.62 && elev >= 0.58 ) || ( barrierV >= 0.82 && elev >= 0.60 ) || directionalGrade >= 0.10 )
: ( ( barrierV >= 0.90 && elev >= 0.62 ) || slopeV >= 0.60 || ( ridgeV >= 0.72 && elev >= 0.64 ) || directionalGrade >= 0.145 ) ;
if ( hardBarrier && passV < ( strictTerrain ? 0.46 : 0.40 ) ) continue ;
const step = dx && dy ? Math . SQRT2 : 1 ;
const terrainCost = strictTerrain
? 1.0
+ slopeV * 12.2
+ ridgeV * 9.0
+ barrierV * 10.8
+ Math . max ( 0 , elev - 0.46 ) * 14.5
+ directionalGrade * 31.0
- ( valleyField ? . [ ni ] || 0 ) * 2.45
- ( plain ? . [ ni ] || 0 ) * 0.92
- passV * 3.10
: 1.0
+ slopeV * 3.65
+ ridgeV * 2.65
+ barrierV * 3.10
+ Math . max ( 0 , elev - 0.58 ) * 5.20
+ directionalGrade * 5.0
- ( valleyField ? . [ ni ] || 0 ) * 0.62
- ( plain ? . [ ni ] || 0 ) * 0.28
- passV * 0.72 ;
const nd = currentDist + step * Math . max ( 0.40 , terrainCost ) ;
const oldDist = distStamp [ ni ] === generation ? dist [ ni ] : INF ;
if ( nd >= oldDist ) continue ;
2026-05-28 15:48:42 +09:00
dist [ ni ] = nd ;
prev [ ni ] = cur ;
2026-08-11 21:51:07 +09:00
distStamp [ ni ] = generation ;
const h = Math . max ( 0 , nextGoalDistance - snapRadius ) * heuristicFloor ;
heap . push ( ni , nd + h ) ;
2026-05-28 15:48:42 +09:00
}
}
2026-08-11 21:51:07 +09:00
if ( hit < 0 ) { failedPrimaryRouteKeys . add ( failureKey ) ; return [ ] ; }
2026-05-28 15:48:42 +09:00
const path = [ ] ;
let cur = hit ;
for ( let guard = 0 ; guard < Math . max ( 80 , maxLength * 3 ) && cur >= 0 ; guard ++ ) {
2026-08-11 21:51:07 +09:00
const x = cur % MAP _W , y = Math . floor ( cur / MAP _W ) ;
2026-05-28 15:48:42 +09:00
path . push ( [ x , y ] ) ;
if ( prev [ cur ] === cur ) break ;
cur = prev [ cur ] ;
}
path . reverse ( ) ;
2026-08-11 21:51:07 +09:00
if ( path . length < 2 || pathLengthCells ( path ) > maxLength ) { failedPrimaryRouteKeys . add ( failureKey ) ; return [ ] ; }
2026-05-28 15:48:42 +09:00
const runs = pathTerrainRuns ( path , terrain ) ;
2026-08-11 21:51:07 +09:00
if ( Number . isFinite ( options . maxSeaRun ) && runs . maxSeaRun > options . maxSeaRun ) { failedPrimaryRouteKeys . add ( failureKey ) ; return [ ] ; }
if ( Number . isFinite ( options . maxTunnelRun ) && runs . maxTunnelRun > options . maxTunnelRun ) { failedPrimaryRouteKeys . add ( failureKey ) ; return [ ] ; }
return path ;
}
function routeLandConnectedTerrainFallback ( a , b , terrain = null , options = { } ) {
if ( ! a || ! b || ! inside ( a . x , a . y ) || ! inside ( b . x , b . y ) ) return [ ] ;
const sea = terrain ? . sea ;
const elevation = terrain ? . elevation ;
const slope = terrain ? . slope ;
const ridgeField = terrain ? . ridgeField ;
const valleyField = terrain ? . valleyField ;
const plain = terrain ? . plain ;
const naturalBarrierScore = terrain ? . naturalBarrierScore ;
const passSuitability = terrain ? . passSuitability ;
const sx = Math . round ( a . x ) , sy = Math . round ( a . y ) ;
const start = indexOf ( sx , sy ) ;
const goal = indexOf ( Math . round ( b . x ) , Math . round ( b . y ) ) ;
if ( sea ? . [ start ] || sea ? . [ goal ] ) return [ ] ;
const direct = Math . hypot ( a . x - b . x , a . y - b . y ) ;
const maxLength = options . maxLength ? ? direct * 4.0 + 160 ;
const snapRadius = 1.5 ;
const bounds = routeSearchBounds ( sx , sy , b . x , b . y , maxLength , snapRadius ) ;
if ( ! bounds ) return [ ] ;
const requestedElevationCeiling = Number . isFinite ( options . maxElevation ) ? options . maxElevation : 0.695 ;
const strictTerrain = options . strictTerrain === true || requestedElevationCeiling <= 0.705 ;
const failureKey = routeFailureKey ( start , goal , maxLength , bounds . cellUpperBound , snapRadius , requestedElevationCeiling , strictTerrain , undefined , undefined ) ;
if ( failedFallbackRouteKeys . has ( failureKey ) ) return [ ] ;
const workspace = fallbackRouteWorkspace ;
const generation = beginRoutingSearch ( workspace ) ;
const { dist , prev , distStamp , closedStamp , metricStamp , startMetric , goalMetric , heap } = workspace ;
dist [ start ] = 0 ; prev [ start ] = start ; distStamp [ start ] = generation ;
metricStamp [ start ] = generation ; startMetric [ start ] = 0 ; goalMetric [ start ] = Math . hypot ( sx - b . x , sy - b . y ) ;
const heuristicFloor = 0.42 ;
heap . push ( start , Math . max ( 0 , goalMetric [ start ] - snapRadius ) * heuristicFloor ) ;
let hit = - 1 ;
let expanded = 0 ;
while ( heap . length && expanded < bounds . cellUpperBound ) {
const cur = heap . pop ( ) ;
if ( cur < 0 || closedStamp [ cur ] === generation ) continue ;
closedStamp [ cur ] = generation ;
expanded ++ ;
const x = cur % MAP _W , y = ( cur / MAP _W ) | 0 ;
const goalDistance = metricStamp [ cur ] === generation ? goalMetric [ cur ] : Math . hypot ( x - b . x , y - b . y ) ;
if ( goalDistance <= snapRadius ) { hit = cur ; break ; }
if ( Math . hypot ( x - a . x , y - a . y ) > maxLength ) continue ;
const currentElevation = elevation ? . [ cur ] || 0 ;
const currentDist = dist [ 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 ( nx < bounds . minX || nx > bounds . maxX || ny < bounds . minY || ny > bounds . maxY ) continue ;
const ni = ny * MAP _W + nx ;
if ( closedStamp [ ni ] === generation || sea ? . [ ni ] ) continue ;
let nextStartDistance , nextGoalDistance ;
if ( metricStamp [ ni ] === generation ) {
nextStartDistance = startMetric [ ni ] ;
nextGoalDistance = goalMetric [ ni ] ;
} else {
nextStartDistance = Math . hypot ( nx - sx , ny - sy ) ;
nextGoalDistance = Math . hypot ( nx - b . x , ny - b . y ) ;
startMetric [ ni ] = nextStartDistance ;
goalMetric [ ni ] = nextGoalDistance ;
metricStamp [ ni ] = generation ;
}
if ( nextStartDistance + nextGoalDistance > bounds . sumLimit + 1e-7 ) continue ;
const elev = elevation ? . [ ni ] || 0 ;
const slopeV = slope ? . [ ni ] || 0 ;
const ridgeV = ridgeField ? . [ ni ] || 0 ;
const barrierV = naturalBarrierScore ? . [ ni ] || 0 ;
const passV = passSuitability ? . [ ni ] || 0 ;
const elevationCeiling = requestedElevationCeiling ;
if ( strictTerrain ) {
if ( elev >= elevationCeiling ) continue ;
if ( elev >= 0.66 && passV < 0.50 ) continue ;
} else if ( elev >= elevationCeiling ) continue ;
const directionalGrade = Math . abs ( elev - currentElevation ) ;
const hardBarrier = strictTerrain
? ( slopeV >= 0.52 || ( ridgeV >= 0.62 && elev >= 0.58 ) || ( barrierV >= 0.82 && elev >= 0.60 ) || directionalGrade >= 0.10 )
: ( ( barrierV >= 0.90 && elev >= 0.62 ) || slopeV >= 0.60 || ( ridgeV >= 0.72 && elev >= 0.64 ) || directionalGrade >= 0.145 ) ;
if ( hardBarrier && passV < ( strictTerrain ? 0.46 : 0.40 ) ) continue ;
const step = dx && dy ? Math . SQRT2 : 1 ;
const terrainCost = strictTerrain
? 1 + slopeV * 12.8 + ridgeV * 9.4 + barrierV * 11.2 + Math . max ( 0 , elev - 0.45 ) * 15.2 + directionalGrade * 33.0 - ( valleyField ? . [ ni ] || 0 ) * 2.60 - ( plain ? . [ ni ] || 0 ) * 0.96 - passV * 3.25
: 1 + slopeV * 4.10 + ridgeV * 3.05 + barrierV * 3.55 + Math . max ( 0 , elev - 0.56 ) * 5.8 + directionalGrade * 5.5 - ( valleyField ? . [ ni ] || 0 ) * 0.72 - ( plain ? . [ ni ] || 0 ) * 0.34 - passV * 0.78 ;
const nd = currentDist + step * Math . max ( 0.42 , terrainCost ) ;
const oldDist = distStamp [ ni ] === generation ? dist [ ni ] : INF ;
if ( nd >= oldDist ) continue ;
dist [ ni ] = nd ; prev [ ni ] = cur ; distStamp [ ni ] = generation ;
heap . push ( ni , nd + Math . max ( 0 , nextGoalDistance - snapRadius ) * heuristicFloor ) ;
}
}
if ( hit < 0 ) { failedFallbackRouteKeys . add ( failureKey ) ; return [ ] ; }
const path = [ ] ;
let cur = hit ;
for ( let guard = 0 ; guard < SIZE && cur >= 0 ; guard ++ ) {
const x = cur % MAP _W , y = Math . floor ( cur / MAP _W ) ; path . push ( [ x , y ] ) ;
if ( prev [ cur ] === cur ) break ;
cur = prev [ cur ] ;
}
path . reverse ( ) ;
if ( path . length < 2 || pathLengthCells ( path ) > maxLength ) { failedFallbackRouteKeys . add ( failureKey ) ; return [ ] ; }
2026-05-28 15:48:42 +09:00
return path ;
}
2026-08-11 21:51:07 +09:00
function terrainFirstConnector ( a , b , terrain = null , options = { } ) {
if ( ! a || ! b ) return [ ] ;
const direct = Math . hypot ( a . x - b . x , a . y - b . y ) ;
// r11.7: there is deliberately no straight-line fallback here. Every
// production transport connector, including short local/IC access, must be
// solved against the terrain raster. If the bounded route search fails, a
// second, broader land-connected terrain search is allowed; fabrication of a
// Euclidean segment is not.
if ( options . skipPrimaryRoute !== true ) {
const routed = routeTerrainPath ( a , b , terrain , {
maxLength : options . maxLength ? ? direct * 2.7 + 48 ,
maxSeaRun : options . maxSeaRun ? ? 0 ,
maxTunnelRun : options . maxTunnelRun ? ? 10 ,
maxExpanded : options . maxExpanded ,
snapRadius : options . snapRadius ? ? 1.8 ,
maxElevation : options . maxElevation ,
strictTerrain : options . strictTerrain ,
} ) ;
if ( routed . length >= 2 ) return routed ;
}
if ( options . allowLandFallback === false ) return [ ] ;
return routeLandConnectedTerrainFallback ( a , b , terrain , {
maxLength : options . maxLength ? ? direct * 3.4 + 72 ,
maxElevation : options . maxElevation ,
strictTerrain : options . strictTerrain ,
} ) ;
}
const nearestPathSpatialStates = [ ] ;
const PATH _SPATIAL _CELL = 8 ;
const PATH _SPATIAL _BINS _X = Math . ceil ( MAP _W / PATH _SPATIAL _CELL ) ;
const PATH _SPATIAL _BINS _Y = Math . ceil ( MAP _H / PATH _SPATIAL _CELL ) ;
function pathSpatialMetaMatches ( state , paths , prefixOnly = false ) {
const count = paths ? . length || 0 ;
if ( prefixOnly ? state . count > count : state . count !== count ) return false ;
for ( let i = 0 ; i < state . count ; i ++ ) {
const path = paths [ i ] ;
if ( state . refs [ i ] !== path ) return false ;
const len = path ? . length || 0 ;
if ( state . lengths [ i ] !== len ) return false ;
const first = path ? . [ 0 ] ;
const mid = path ? . [ len ? Math . floor ( ( len - 1 ) * 0.5 ) : 0 ] ;
const last = path ? . [ len - 1 ] ;
if ( ( state . firstX [ i ] !== ( first ? . [ 0 ] ? ? NaN ) ) || ( state . firstY [ i ] !== ( first ? . [ 1 ] ? ? NaN ) )
|| ( state . midX [ i ] !== ( mid ? . [ 0 ] ? ? NaN ) ) || ( state . midY [ i ] !== ( mid ? . [ 1 ] ? ? NaN ) )
|| ( state . lastX [ i ] !== ( last ? . [ 0 ] ? ? NaN ) ) || ( state . lastY [ i ] !== ( last ? . [ 1 ] ? ? NaN ) ) ) return false ;
}
return true ;
}
function recordPathMeta ( state , index , path ) {
const len = path ? . length || 0 ;
const first = path ? . [ 0 ] ;
const mid = path ? . [ len ? Math . floor ( ( len - 1 ) * 0.5 ) : 0 ] ;
const last = path ? . [ len - 1 ] ;
state . refs [ index ] = path ;
state . lengths [ index ] = len ;
state . firstX [ index ] = first ? . [ 0 ] ? ? NaN ; state . firstY [ index ] = first ? . [ 1 ] ? ? NaN ;
state . midX [ index ] = mid ? . [ 0 ] ? ? NaN ; state . midY [ index ] = mid ? . [ 1 ] ? ? NaN ;
state . lastX [ index ] = last ? . [ 0 ] ? ? NaN ; state . lastY [ index ] = last ? . [ 1 ] ? ? NaN ;
return len ;
}
function appendPathsToSpatialState ( state , paths , fromIndex ) {
for ( let pathIndex = fromIndex ; pathIndex < ( paths ? . length || 0 ) ; pathIndex ++ ) {
const path = paths [ pathIndex ] ;
recordPathMeta ( state , pathIndex , path ) ;
for ( const point of path || [ ] ) {
if ( ! point ) continue ;
const x = point [ 0 ] , y = point [ 1 ] ;
if ( ! Number . isFinite ( x ) || ! Number . isFinite ( y ) || x < 0 || y < 0 || x >= MAP _W || y >= MAP _H ) continue ;
const bx = Math . max ( 0 , Math . min ( PATH _SPATIAL _BINS _X - 1 , Math . floor ( x / PATH _SPATIAL _CELL ) ) ) ;
const by = Math . max ( 0 , Math . min ( PATH _SPATIAL _BINS _Y - 1 , Math . floor ( y / PATH _SPATIAL _CELL ) ) ) ;
const binIndex = by * PATH _SPATIAL _BINS _X + bx ;
let bucket = state . bins [ binIndex ] ;
if ( ! bucket ) state . bins [ binIndex ] = bucket = [ ] ;
bucket . push ( x , y ) ;
}
}
state . count = paths ? . length || 0 ;
}
function spatialStateForPaths ( paths ) {
const rows = paths || [ ] ;
// Exact/prefix matching by underlying path references means spread-created
// network arrays still share one index. Growing networks append only the new
// paths instead of rescanning every old road vertex.
let prefixState = null ;
for ( let i = 0 ; i < nearestPathSpatialStates . length ; i ++ ) {
const state = nearestPathSpatialStates [ i ] ;
if ( pathSpatialMetaMatches ( state , rows , false ) ) {
if ( i > 0 ) { nearestPathSpatialStates . splice ( i , 1 ) ; nearestPathSpatialStates . unshift ( state ) ; }
return state ;
}
if ( ( ! prefixState || state . count > prefixState . count ) && pathSpatialMetaMatches ( state , rows , true ) ) prefixState = state ;
}
if ( prefixState ) {
appendPathsToSpatialState ( prefixState , rows , prefixState . count ) ;
const idx = nearestPathSpatialStates . indexOf ( prefixState ) ;
if ( idx > 0 ) { nearestPathSpatialStates . splice ( idx , 1 ) ; nearestPathSpatialStates . unshift ( prefixState ) ; }
return prefixState ;
}
const state = {
bins : new Array ( PATH _SPATIAL _BINS _X * PATH _SPATIAL _BINS _Y ) ,
refs : [ ] , lengths : [ ] , firstX : [ ] , firstY : [ ] , midX : [ ] , midY : [ ] , lastX : [ ] , lastY : [ ] , count : 0 ,
} ;
appendPathsToSpatialState ( state , rows , 0 ) ;
nearestPathSpatialStates . unshift ( state ) ;
if ( nearestPathSpatialStates . length > 10 ) nearestPathSpatialStates . pop ( ) ;
return state ;
}
2026-05-28 00:30:09 +09:00
function nearestPointOnPaths ( paths , p , maxDistance = Infinity ) {
2026-08-11 21:51:07 +09:00
if ( ! paths ? . length || ! p ) return null ;
if ( ! Number . isFinite ( maxDistance ) ) {
let best = null ;
for ( const path of paths || [ ] ) for ( const [ x , y ] of path || [ ] ) {
const d = Math . hypot ( p . x - x , p . y - y ) ;
if ( ! best || d < best . d ) best = { x , y , d } ;
}
return best ;
}
const state = spatialStateForPaths ( paths ) ;
const minBx = Math . max ( 0 , Math . floor ( ( p . x - maxDistance ) / PATH _SPATIAL _CELL ) ) ;
const maxBx = Math . min ( PATH _SPATIAL _BINS _X - 1 , Math . floor ( ( p . x + maxDistance ) / PATH _SPATIAL _CELL ) ) ;
const minBy = Math . max ( 0 , Math . floor ( ( p . y - maxDistance ) / PATH _SPATIAL _CELL ) ) ;
const maxBy = Math . min ( PATH _SPATIAL _BINS _Y - 1 , Math . floor ( ( p . y + maxDistance ) / PATH _SPATIAL _CELL ) ) ;
let bestX = 0 , bestY = 0 ;
let bestD2 = maxDistance * maxDistance ;
let found = false ;
for ( let by = minBy ; by <= maxBy ; by ++ ) {
for ( let bx = minBx ; bx <= maxBx ; bx ++ ) {
const bucket = state . bins [ by * PATH _SPATIAL _BINS _X + bx ] ;
if ( ! bucket ) continue ;
for ( let k = 0 ; k < bucket . length ; k += 2 ) {
const x = bucket [ k ] , y = bucket [ k + 1 ] ;
const dx = p . x - x , dy = p . y - y ;
const d2 = dx * dx + dy * dy ;
if ( d2 <= bestD2 ) { bestD2 = d2 ; bestX = x ; bestY = y ; found = true ; }
}
}
}
return found ? { x : bestX , y : bestY , d : Math . sqrt ( bestD2 ) } : null ;
}
function trimRouteAtExistingNetwork ( path , network , radius = 2.6 , minTravelCells = 4 ) {
if ( ! path ? . length || ! network ? . length ) return path || [ ] ;
const minIndex = Math . min ( path . length - 1 , Math . max ( 1 , Math . floor ( minTravelCells ) ) ) ;
for ( let k = minIndex ; k < path . length ; k ++ ) {
const [ x , y ] = path [ k ] ;
const hit = nearestPointOnPaths ( network , { x , y } , radius ) ;
if ( ! hit ) continue ;
const out = path . slice ( 0 , k + 1 ) ;
const last = out [ out . length - 1 ] ;
const snapGap = Math . hypot ( last [ 0 ] - hit . x , last [ 1 ] - hit . y ) ;
// Never manufacture a straight connector merely to make the endpoint touch
// the existing network. A sub-cell raster snap is harmless; any larger gap
// must remain a proximity connection or be routed explicitly elsewhere.
if ( snapGap > 0.75 && snapGap <= 1.55 ) out . push ( [ Math . round ( hit . x ) , Math . round ( hit . y ) ] ) ;
return out ;
}
return path ;
}
function nearestPointsOnPaths ( paths , p , maxDistance = Infinity , limit = 12 , stride = 3 ) {
const rows = [ ] ;
2026-05-28 00:30:09 +09:00
for ( const path of paths || [ ] ) {
2026-08-11 21:51:07 +09:00
for ( let k = 0 ; k < ( path ? . length || 0 ) ; k += Math . max ( 1 , stride ) ) {
const [ x , y ] = path [ k ] ;
2026-05-28 00:30:09 +09:00
const d = Math . hypot ( p . x - x , p . y - y ) ;
2026-08-11 21:51:07 +09:00
if ( d <= maxDistance ) rows . push ( { x , y , d } ) ;
2026-05-28 00:30:09 +09:00
}
}
2026-08-11 21:51:07 +09:00
rows . sort ( ( a , b ) => a . d - b . d ) ;
const out = [ ] ;
for ( const row of rows ) {
if ( out . some ( ( q ) => Math . hypot ( q . x - row . x , q . y - row . y ) < 7 ) ) continue ;
out . push ( row ) ;
if ( out . length >= limit ) break ;
}
return out ;
2026-05-28 00:30:09 +09:00
}
function nearestEntity ( entities , p , maxDistance = Infinity ) {
let best = null ;
for ( const q of entities || [ ] ) {
if ( ! q || ! Number . isFinite ( q . x ) || ! Number . isFinite ( q . y ) || ( q . x === p . x && q . y === p . y ) ) continue ;
const d = Math . hypot ( q . x - p . x , q . y - p . y ) ;
if ( d <= maxDistance && ( ! best || d < best . d ) ) best = { ... q , d } ;
}
return best ;
}
function dedupePaths ( paths , sampleStep = 2 ) {
2026-08-11 21:51:07 +09:00
const normalized = normalizeTransportPathSet ( paths , { sampleStep , mutate : true } ) ;
return Array . isArray ( paths ) ? paths : normalized . paths ;
2026-05-28 00:30:09 +09:00
}
function addInterchange ( interchanges , x , y , source = "post-admin-expressway-endpoint" ) {
x = Math . round ( x ) ; y = Math . round ( y ) ;
if ( ! inside ( x , y ) ) return false ;
if ( ( interchanges || [ ] ) . some ( ( p ) => Math . hypot ( p . x - x , p . y - y ) <= 2.5 ) ) return false ;
interchanges . push ( { x , y , kind : "Interchange" , score : 1 , source } ) ;
return true ;
}
function smoothPath ( path , passes = 1 ) {
2026-08-08 17:41:30 +09:00
return smoothRasterPath ( path , passes ) ;
2026-05-28 00:30:09 +09:00
}
2026-08-11 21:51:07 +09:00
function pathTangent ( path , k , span = 2 ) {
const a = path ? . [ Math . max ( 0 , k - span ) ] || path ? . [ k ] || [ 0 , 0 ] ;
const b = path ? . [ Math . min ( ( path ? . length || 1 ) - 1 , k + span ) ] || path ? . [ k ] || [ 0 , 0 ] ;
const dx = b [ 0 ] - a [ 0 ] , dy = b [ 1 ] - a [ 1 ] ;
const d = Math . hypot ( dx , dy ) || 1 ;
return [ dx / d , dy / d ] ;
}
function pathSharpTurnStats ( path ) {
let turns = 0 , sharp = 0 , extreme = 0 , consecutiveExtreme = 0 , run = 0 , maxRun = 0 ;
for ( let k = 2 ; k < ( path ? . length || 0 ) - 2 ; k += 2 ) {
const a = path [ k - 2 ] , b = path [ k ] , c = path [ k + 2 ] ;
if ( ! a || ! b || ! c ) continue ;
const ux = b [ 0 ] - a [ 0 ] , uy = b [ 1 ] - a [ 1 ] , vx = c [ 0 ] - b [ 0 ] , vy = c [ 1 ] - b [ 1 ] ;
const ud = Math . hypot ( ux , uy ) , vd = Math . hypot ( vx , vy ) ;
if ( ! ud || ! vd ) continue ;
const dot = Math . max ( - 1 , Math . min ( 1 , ( ux * vx + uy * vy ) / ( ud * vd ) ) ) ;
const angle = Math . acos ( dot ) * 180 / Math . PI ;
turns ++ ;
if ( angle >= 50 ) sharp ++ ;
if ( angle >= 82 ) { extreme ++ ; run ++ ; maxRun = Math . max ( maxRun , run ) ; }
else run = 0 ;
}
consecutiveExtreme = maxRun ;
return { turns , sharp , extreme , consecutiveExtreme , sharpShare : turns ? sharp / turns : 0 } ;
}
function trunkElevationSafe ( path , terrain , ceiling = 0.72 ) {
if ( ! path ? . length ) return false ;
for ( const [ x , y ] of path ) {
if ( ! inside ( x , y ) ) return false ;
if ( ( terrain ? . elevation ? . [ indexOf ( x , y ) ] || 0 ) > ceiling ) return false ;
}
return true ;
}
function terrainSafeSmooth ( path , terrain , mode = "national" , passes = 2 ) {
if ( ! path || path . length < 5 ) return path || [ ] ;
let best = path ;
const maxTunnelRun = mode === "rail" ? 24 : mode === "expressway" ? 24 : mode === "national" ? 18 : 8 ;
const maxSeaRun = mode === "expressway" ? 3 : mode === "rail" ? 2 : 2 ;
const baseStats = pathSharpTurnStats ( path ) ;
for ( let p = 1 ; p <= passes ; p ++ ) {
const candidate = smoothRasterPath ( path , p ) ;
if ( ! candidate ? . length ) continue ;
const runs = pathTerrainRuns ( candidate , terrain ) ;
const burden = pathTerrainBurden ( candidate , terrain ) ;
if ( runs . maxTunnelRun > maxTunnelRun || runs . maxSeaRun > maxSeaRun ) continue ;
// Smoothing must never be the stage that cuts a corner over water or a
// mountain. Trunk candidates are checked cell-for-cell against the same
// strict terrain contract as the final sanitizer.
if ( [ "national" , "expressway" , "rail" ] . includes ( mode ) ) {
let invalid = false ;
for ( const [ x , y ] of candidate ) {
if ( ! inside ( x , y ) ) { invalid = true ; break ; }
2026-05-28 00:30:09 +09:00
const i = indexOf ( x , y ) ;
2026-08-11 21:51:07 +09:00
const passV = terrain ? . passSuitability ? . [ i ] || 0 ;
const elevationV = terrain ? . elevation ? . [ i ] || 0 ;
const mountainObstacle = ( terrain ? . slope ? . [ i ] || 0 ) >= 0.52
|| ( ( terrain ? . ridgeField ? . [ i ] || 0 ) >= 0.62 && elevationV >= 0.58 )
|| ( ( terrain ? . naturalBarrierScore ? . [ i ] || 0 ) >= 0.82 && elevationV >= 0.60 ) ;
if ( terrain ? . sea ? . [ i ] || elevationV >= 0.695 || ( mountainObstacle && passV < 0.46 ) ) { invalid = true ; break ; }
2026-05-28 00:30:09 +09:00
}
2026-08-11 21:51:07 +09:00
if ( invalid ) continue ;
2026-05-28 00:30:09 +09:00
}
2026-08-11 21:51:07 +09:00
if ( burden . highBarrierShare > ( mode === "rail" ? 0.10 : mode === "expressway" ? 0.08 : mode === "national" ? 0.10 : 0.30 ) ) continue ;
const stats = pathSharpTurnStats ( candidate ) ;
if ( stats . extreme <= pathSharpTurnStats ( best ) . extreme && stats . sharpShare <= Math . max ( 0.34 , baseStats . sharpShare + 0.04 ) ) best = candidate ;
2026-05-28 00:30:09 +09:00
}
2026-08-11 21:51:07 +09:00
return best ;
}
function pathServesMajorCity ( path , city , mode ) {
if ( ! path || ! city ) return false ;
if ( mode !== "expressway" ) return pathTouchesCell ( path , city . x , city . y , mode === "rail" ? 2.2 : 2.0 ) ;
const inner = Math . max ( 6.5 , ( city . coreRadius || 4 ) + 4.0 ) ;
const outer = Math . max ( inner + 6 , ( city . urbanRadius || 12 ) * 2.1 ) ;
const largeCity = ( city . population || 0 ) >= 180000 ;
const largeCityReach = Math . max ( 12 , Math . min ( 24 , ( city . urbanRadius || 12 ) * 1.55 ) ) ;
let inBand = false , outside = false , minD = Infinity , maxD = 0 ;
for ( const [ x , y ] of path ) {
const d = Math . hypot ( x - city . x , y - city . y ) ;
minD = Math . min ( minD , d ) ;
maxD = Math . max ( maxD , d ) ;
if ( d >= inner && d <= outer ) inBand = true ;
if ( d >= Math . max ( 20 , outer * 0.85 ) ) outside = true ;
}
// Several-hundred-thousand-person cities require an actual suburban
// approach. A motorway only touching the remote metropolitan fringe must not
// count as unique service during final pruning/alignment decisions.
if ( largeCity ) return minD <= largeCityReach && pathLengthCells ( path ) >= 12 && maxD - minD >= 8 ;
return inBand && outside ;
}
// Direction-aware final anti-parallel pass. The old raster-overlap test treated
// crossings and parallel roads alike and, more importantly, ran before the
// post-admin service links were appended. This pass executes on the completed
// hierarchy and specifically removes kilometre-scale side-by-side corridors.
function pruneFinalParallelPaths ( paths , mode , majorCities = [ ] , options = { } ) {
if ( ! Array . isArray ( paths ) || paths . length < 2 ) return { before : paths ? . length || 0 , after : paths ? . length || 0 , pruned : 0 } ;
const before = paths . length ;
const radius = options . radius ? ? ( mode === "expressway" ? 4 : mode === "national" ? 3 : 3 ) ;
const threshold = options . threshold ? ? ( mode === "expressway" ? 0.30 : mode === "national" ? 0.38 : 0.44 ) ;
const dotFloor = options . directionDot ? ? 0.90 ;
const sampleStride = 2 ;
const rows = paths . map ( ( path , index ) => {
const served = new Set ( ) ;
majorCities . forEach ( ( city , ci ) => { if ( pathServesMajorCity ( path , city , mode ) ) served . add ( ci ) ; } ) ;
return { path , index , served , len : pathLengthCells ( path ) , score : served . size * 120 + pathLengthCells ( path ) } ;
} ) . sort ( ( a , b ) => b . score - a . score || b . len - a . len ) ;
const accepted = [ ] ;
const acceptedSamples = [ ] ;
const servedByAccepted = new Set ( ) ;
function samples ( path ) {
const out = [ ] ;
for ( let k = 0 ; k < ( path ? . length || 0 ) ; k += sampleStride ) {
const p = path [ k ] ;
if ( ! p ) continue ;
const [ tx , ty ] = pathTangent ( path , k , 2 ) ;
out . push ( { x : p [ 0 ] , y : p [ 1 ] , tx , ty } ) ;
}
return out ;
}
function parallelStats ( ss ) {
let hit = 0 ;
let run = 0 ;
let longestRun = 0 ;
for ( const p of ss ) {
let parallel = false ;
for ( const q of acceptedSamples ) {
const dx = p . x - q . x , dy = p . y - q . y ;
if ( Math . abs ( dx ) > radius || Math . abs ( dy ) > radius || dx * dx + dy * dy > radius * radius ) continue ;
if ( Math . abs ( p . tx * q . tx + p . ty * q . ty ) < dotFloor ) continue ;
parallel = true ; break ;
}
if ( parallel ) { hit ++ ; run ++ ; longestRun = Math . max ( longestRun , run ) ; }
else run = 0 ;
}
return { share : ss . length ? hit / ss . length : 0 , longestRun } ;
}
for ( const row of rows ) {
const ss = samples ( row . path ) ;
const { share , longestRun } = parallelStats ( ss ) ;
const uniqueService = [ ... row . served ] . some ( ( id ) => ! servedByAccepted . has ( id ) ) ;
const longEnough = row . len >= ( mode === "expressway" ? 16 : 12 ) ;
// Whole-path overlap misses a common failure mode: a route can parallel a
// trunk only for a 5-10 km segment, then diverge, yielding a modest global
// share. Treat sustained local parallelism as redundant as well. With the
// 2-cell sample stride, five samples are roughly five kilometres.
const maxParallelRunSamples = options . maxParallelRunSamples ? ? ( mode === "expressway" ? 5 : 6 ) ;
const sustainedParallel = longestRun >= maxParallelRunSamples ;
if ( accepted . length && longEnough && ( share > threshold || sustainedParallel ) && ! uniqueService ) continue ;
accepted . push ( row ) ;
acceptedSamples . push ( ... ss ) ;
for ( const id of row . served ) servedByAccepted . add ( id ) ;
}
accepted . sort ( ( a , b ) => a . index - b . index ) ;
paths . length = 0 ;
paths . push ( ... accepted . map ( ( row ) => row . path ) ) ;
return { before , after : paths . length , pruned : before - paths . length , radius , threshold , directionAware : true , maxParallelRunSamples : options . maxParallelRunSamples ? ? ( mode === "expressway" ? 5 : 6 ) } ;
}
// When two trunk routes are both semantically required, deleting the lower
// priority path can break city service. Instead, collapse a sustained
// near-parallel section onto the already accepted corridor. The routes then
// share one physical alignment and diverge only where their destinations do.
// This models multiplexed Japanese trunk corridors much better than drawing two
// highways one or two cells apart for kilometres.
function collapseParallelCorridorsOntoSharedAlignment ( paths , mode , majorCities = [ ] , terrain = null , options = { } ) {
if ( ! Array . isArray ( paths ) || paths . length < 2 ) return { inspected : paths ? . length || 0 , collapsed : 0 , cellsReused : 0 } ;
const radius = options . radius ? ? 2 ;
const dotFloor = options . directionDot ? ? 0.90 ;
const minRunPoints = options . minRunPoints ? ? ( mode === "expressway" ? 5 : 6 ) ;
const rows = paths . map ( ( path , index ) => {
let service = 0 ;
for ( const city of majorCities || [ ] ) if ( pathServesMajorCity ( path , city , mode ) ) service ++ ;
return { index , path , score : service * 200 + pathLengthCells ( path ) , service } ;
} ) . sort ( ( a , b ) => b . score - a . score || b . path . length - a . path . length || a . index - b . index ) ;
const accepted = [ ] ;
let collapsed = 0 , cellsReused = 0 ;
function nearestParallel ( path , k , ref ) {
const p = path [ k ] ;
const [ tx , ty ] = pathTangent ( path , k , 2 ) ;
let best = null ;
for ( let q = 0 ; q < ref . length ; q ++ ) {
const z = ref [ q ] ;
const dx = p [ 0 ] - z [ 0 ] , dy = p [ 1 ] - z [ 1 ] ;
const d2 = dx * dx + dy * dy ;
// Already-shared cells are the desired multiplexed state, not a
// distinct parallel corridor. Ignore them here so an existing shared
// section cannot mask a nearby side-by-side run that still needs to be
// collapsed. This matches the topology auditor's definition.
if ( d2 < 0.75 || d2 > radius * radius ) continue ;
const [ ux , uy ] = pathTangent ( ref , q , 2 ) ;
const dot = tx * ux + ty * uy ;
if ( Math . abs ( dot ) < dotFloor ) continue ;
if ( ! best || d2 < best . d2 ) best = { q , d2 , sign : dot >= 0 ? 1 : - 1 } ;
}
return best ;
}
function bestRun ( path , ref ) {
let current = null , best = null ;
for ( let k = 0 ; k < path . length ; k ++ ) {
const hit = nearestParallel ( path , k , ref ) ;
if ( ! hit ) { current = null ; continue ; }
const compatible = current
&& current . sign === hit . sign
&& ( hit . sign > 0 ? hit . q >= current . lastQ - 2 : hit . q <= current . lastQ + 2 )
&& Math . abs ( hit . q - current . lastQ ) <= 6 ;
if ( ! compatible ) current = { start : k , end : k , startQ : hit . q , endQ : hit . q , lastQ : hit . q , sign : hit . sign , count : 1 } ;
else { current . end = k ; current . endQ = hit . q ; current . lastQ = hit . q ; current . count ++ ; }
if ( ! best || current . count > best . count ) best = { ... current } ;
}
return best && best . count >= minRunPoints ? best : null ;
}
function mergeOnReference ( path , ref , run ) {
let q0 = run . startQ , q1 = run . endQ ;
let shared ;
if ( run . sign >= 0 ) {
if ( q1 < q0 ) [ q0 , q1 ] = [ q1 , q0 ] ;
shared = ref . slice ( q0 , q1 + 1 ) ;
} else {
if ( q0 < q1 ) [ q0 , q1 ] = [ q1 , q0 ] ;
shared = ref . slice ( q1 , q0 + 1 ) . reverse ( ) ;
}
if ( shared . length < 2 ) return null ;
const prefix = path . slice ( 0 , run . start ) ;
const suffix = path . slice ( run . end + 1 ) ;
const pieces = [ ] ;
if ( prefix . length ) pieces . push ( prefix ) ;
const prefixEnd = prefix [ prefix . length - 1 ] ;
if ( prefixEnd ) {
const d = Math . hypot ( prefixEnd [ 0 ] - shared [ 0 ] [ 0 ] , prefixEnd [ 1 ] - shared [ 0 ] [ 1 ] ) ;
const connector = terrainFirstConnector (
{ x : prefixEnd [ 0 ] , y : prefixEnd [ 1 ] } , { x : shared [ 0 ] [ 0 ] , y : shared [ 0 ] [ 1 ] } , terrain ,
{ maxLength : Math . max ( 8 , d * 3.0 + 8 ) , maxSeaRun : 0 , maxTunnelRun : 0 , maxElevation : 0.695 , snapRadius : 0.8 }
) ;
if ( d > 0.75 && connector . length < 2 ) return null ;
if ( connector . length >= 2 ) pieces . push ( connector . slice ( 1 ) ) ;
}
pieces . push ( shared ) ;
const suffixStart = suffix [ 0 ] ;
if ( suffixStart ) {
const tail = shared [ shared . length - 1 ] ;
const d = Math . hypot ( tail [ 0 ] - suffixStart [ 0 ] , tail [ 1 ] - suffixStart [ 1 ] ) ;
const connector = terrainFirstConnector (
{ x : tail [ 0 ] , y : tail [ 1 ] } , { x : suffixStart [ 0 ] , y : suffixStart [ 1 ] } , terrain ,
{ maxLength : Math . max ( 8 , d * 3.0 + 8 ) , maxSeaRun : 0 , maxTunnelRun : 0 , maxElevation : 0.695 , snapRadius : 0.8 }
) ;
if ( d > 0.75 && connector . length < 2 ) return null ;
if ( connector . length >= 2 ) pieces . push ( connector . slice ( 1 ) ) ;
}
if ( suffix . length ) pieces . push ( suffix . slice ( 1 ) ) ;
const merged = [ ] ;
for ( const piece of pieces ) for ( const pt of piece || [ ] ) {
const last = merged [ merged . length - 1 ] ;
if ( ! last || last [ 0 ] !== pt [ 0 ] || last [ 1 ] !== pt [ 1 ] ) merged . push ( pt ) ;
}
if ( merged . length < 3 ) return null ;
const runs = pathTerrainRuns ( merged , terrain ) ;
const turnStats = pathSharpTurnStats ( merged ) ;
if ( mode === "expressway" && ( runs . maxSeaRun > 0 || runs . maxTunnelRun > 24 || turnStats . consecutiveExtreme > 1 || ! trunkElevationSafe ( merged , terrain , 0.72 ) ) ) return null ;
if ( mode === "national" && ( runs . maxSeaRun > 0 || runs . maxTunnelRun > 18 || turnStats . consecutiveExtreme > 1 || ! trunkElevationSafe ( merged , terrain , 0.74 ) ) ) return null ;
return merged ;
}
for ( const row of rows ) {
let path = row . path ;
let best = null ;
for ( const ref of accepted ) {
const run = bestRun ( path , ref ) ;
if ( ! run ) continue ;
if ( ! best || run . count > best . run . count ) best = { ref , run } ;
}
if ( best ) {
const merged = mergeOnReference ( path , best . ref , best . run ) ;
if ( merged ) {
paths [ row . index ] = path = merged ;
collapsed ++ ;
cellsReused += best . run . count ;
}
}
accepted . push ( path ) ;
}
return { inspected : paths . length , collapsed , cellsReused , radius , minRunPoints } ;
}
// Remove path objects that remain as short duplicate loops/spurs after nearby
// corridors have been snapped onto one physical alignment. `dedupePaths()` only
// catches near-identical whole paths; a common failure was two trunk routes
// sharing 70-90% of their cells in reverse order with a tiny unique tail, which
// rendered as tangled double junctions. Keep the higher-value/longer route and
// discard the short redundant object unless it is the sole service for a major
// city.
function pruneNearDuplicateTrunkPaths ( paths , mode , majorCities = [ ] , options = { } ) {
if ( ! Array . isArray ( paths ) || paths . length < 2 ) return { before : paths ? . length || 0 , after : paths ? . length || 0 , pruned : 0 } ;
const before = paths . length ;
const overlapFloor = options . overlapFloor ? ? ( mode === "expressway" ? 0.48 : 0.52 ) ;
const maxUniqueCells = options . maxUniqueCells ? ? ( mode === "expressway" ? 12 : 10 ) ;
const rows = paths . map ( ( path , index ) => {
const served = new Set ( ) ;
for ( let ci = 0 ; ci < ( majorCities || [ ] ) . length ; ci ++ ) if ( pathServesMajorCity ( path , majorCities [ ci ] , mode ) ) served . add ( ci ) ;
return { path , index , served , len : pathLengthCells ( path ) } ;
} ) . sort ( ( a , b ) => b . served . size - a . served . size || b . len - a . len || a . index - b . index ) ;
const accepted = [ ] ;
const occupied = new Set ( ) ;
const serviceOwners = new Set ( ) ;
let pruned = 0 ;
for ( const row of rows ) {
let overlap = 0 ;
const uniqueKeys = new Set ( ) ;
for ( const point of row . path || [ ] ) {
if ( ! point ) continue ;
const key = ` ${ point [ 0 ] } , ${ point [ 1 ] } ` ;
if ( occupied . has ( key ) ) overlap ++ ;
else uniqueKeys . add ( key ) ;
}
const share = row . path ? . length ? overlap / row . path . length : 0 ;
const uniqueService = [ ... row . served ] . some ( ( id ) => ! serviceOwners . has ( id ) ) ;
const redundant = accepted . length > 0
&& share >= overlapFloor
&& ( uniqueKeys . size <= maxUniqueCells || share >= 0.78 )
&& ! uniqueService ;
if ( redundant ) { pruned ++ ; continue ; }
accepted . push ( row ) ;
for ( const point of row . path || [ ] ) if ( point ) occupied . add ( ` ${ point [ 0 ] } , ${ point [ 1 ] } ` ) ;
for ( const id of row . served ) serviceOwners . add ( id ) ;
}
accepted . sort ( ( a , b ) => a . index - b . index ) ;
paths . length = 0 ;
paths . push ( ... accepted . map ( ( row ) => row . path ) ) ;
return { before , after : paths . length , pruned , overlapFloor , maxUniqueCells } ;
}
const incrementalInfluenceStates = [ ] ;
function appendPathsToInfluenceState ( state , paths , fromIndex ) {
for ( let i = fromIndex ; i < ( paths ? . length || 0 ) ; i ++ ) {
const path = paths [ i ] ;
recordPathMeta ( state , i , path ) ;
state . accumulator . add ( path , 1 , state . radius ) ;
}
state . count = paths ? . length || 0 ;
}
function rebuildInfluence ( paths , radius = 5 ) {
const rows = paths || [ ] ;
let prefixState = null ;
for ( let i = 0 ; i < incrementalInfluenceStates . length ; i ++ ) {
const state = incrementalInfluenceStates [ i ] ;
if ( state . radius !== radius ) continue ;
if ( pathSpatialMetaMatches ( state , rows , false ) ) {
if ( i > 0 ) { incrementalInfluenceStates . splice ( i , 1 ) ; incrementalInfluenceStates . unshift ( state ) ; }
return state . accumulator . field ;
}
if ( ( ! prefixState || state . count > prefixState . count ) && pathSpatialMetaMatches ( state , rows , true ) ) prefixState = state ;
}
if ( prefixState ) {
appendPathsToInfluenceState ( prefixState , rows , prefixState . count ) ;
const idx = incrementalInfluenceStates . indexOf ( prefixState ) ;
if ( idx > 0 ) { incrementalInfluenceStates . splice ( idx , 1 ) ; incrementalInfluenceStates . unshift ( prefixState ) ; }
return prefixState . accumulator . field ;
}
const state = {
radius ,
accumulator : createIncrementalPathInfluence ( [ ] , radius , { exponent : 1.0 } ) ,
refs : [ ] , lengths : [ ] , firstX : [ ] , firstY : [ ] , midX : [ ] , midY : [ ] , lastX : [ ] , lastY : [ ] , count : 0 ,
} ;
appendPathsToInfluenceState ( state , rows , 0 ) ;
incrementalInfluenceStates . unshift ( state ) ;
if ( incrementalInfluenceStates . length > 12 ) incrementalInfluenceStates . pop ( ) ;
return state . accumulator . field ;
2026-05-28 00:30:09 +09:00
}
2026-08-11 21:51:07 +09:00
export function finalizeAdminAwareTransport ( { seed , terrain , features , admin , initialVisibleCrop = null } ) {
2026-05-28 00:30:09 +09:00
if ( ! features || ! admin ) return features ;
2026-08-11 21:51:07 +09:00
// Route failures depend on this terrain raster; never carry memoized failures
// into another generated map in the same JS realm.
failedPrimaryRouteKeys . clear ( ) ;
failedFallbackRouteKeys . clear ( ) ;
nearestPathSpatialStates . length = 0 ;
incrementalInfluenceStates . length = 0 ;
2026-05-28 00:30:09 +09:00
const minorRoads = features . minorRoads || [ ] ;
const nationalRoads = features . nationalRoads || [ ] ;
const externalRoads = features . externalRoads || [ ] ;
const expressways = features . expressways || [ ] ;
const externalExpressways = features . externalExpressways || [ ] ;
const interchanges = features . interchanges || [ ] ;
const adminCenters = admin . adminCentersRaw || features . adminCenters || [ ] ;
const townsForNational = [
... ( features . modernCities || [ ] ) . filter ( ( p ) => ( p . population || 0 ) >= 5000 ) ,
... ( features . markets || [ ] ) . filter ( ( p ) => ( p . population || 0 ) >= 5000 ) ,
... ( features . villages || [ ] ) . filter ( ( p ) => ( p . population || 0 ) >= 5000 ) ,
... ( features . ports || [ ] ) . filter ( ( p ) => ( p . population || 0 ) >= 5000 || p . portClass === "major" || p . portClass === "regional" || p . portClass === "fishing" ) ,
] ;
2026-08-11 21:51:07 +09:00
const debug = { order : "settlements -> administration -> transport" , terrainFirstLongDistanceRouting : true , narrowStraitBridgeAllowanceCells : { national : 2 , expressway : 3 , rail : 2 } , majorCityAllTrunkFloor : 50000 , adminCentersChecked : 0 , adminLocalRoadsAdded : 0 , nationalTownSpursAdded : 0 , nationalTownChainsAdded : 0 , nationalTownChainTownsCovered : 0 , expresswayEndpointInterchangesAdded : 0 , expresswaysSmoothed : 0 , expresswayMajorCityLinksAdded : 0 , railCityChainsAdded : 0 , railCityChainCitiesCovered : 0 } ;
// Cache 8-neighbour land components once. Major-city trunk guarantees use it
// to distinguish a genuine large-strait exception from a routing failure and
// to prevent a suburban anchor from accidentally landing on a nearby island.
const landComponentId = new Int32Array ( SIZE ) ;
landComponentId . fill ( - 1 ) ;
let landComponentCount = 0 ;
for ( let i = 0 ; i < SIZE ; i ++ ) {
if ( terrain ? . sea ? . [ i ] || landComponentId [ i ] >= 0 ) continue ;
const id = landComponentCount ++ ;
const queue = [ i ] ; landComponentId [ i ] = id ;
for ( let qi = 0 ; qi < queue . length ; qi ++ ) {
const cur = queue [ qi ] ; 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 ( terrain ? . sea ? . [ ni ] || landComponentId [ ni ] >= 0 ) continue ;
landComponentId [ ni ] = id ; queue . push ( ni ) ;
}
}
}
const landComponentArea = new Int32Array ( Math . max ( 1 , landComponentCount ) ) ;
for ( let i = 0 ; i < SIZE ; i ++ ) if ( landComponentId [ i ] >= 0 ) landComponentArea [ landComponentId [ i ] ] ++ ;
const componentAt = ( p ) => p && inside ( Math . round ( p . x ) , Math . round ( p . y ) ) ? landComponentId [ indexOf ( Math . round ( p . x ) , Math . round ( p . y ) ) ] : - 1 ;
const sameLandComponent = ( a , b ) => componentAt ( a ) >= 0 && componentAt ( a ) === componentAt ( b ) ;
const civicTransportTargets = [
... ( features . modernCities || [ ] ) , ... ( features . markets || [ ] ) , ... ( features . ports || [ ] ) ,
... ( features . villages || [ ] ) , ... ( adminCenters || [ ] ) , ... ( features . externalGateways || [ ] ) ,
] . filter ( ( p ) => p && Number . isFinite ( p . x ) && Number . isFinite ( p . y ) && inside ( Math . round ( p . x ) , Math . round ( p . y ) ) && ! terrain ? . sea ? . [ indexOf ( Math . round ( p . x ) , Math . round ( p . y ) ) ] ) ;
function sameComponentCivicTargets ( origin , options = { } ) {
const component = componentAt ( origin ) ;
const minDistance = options . minDistance ? ? 7 ;
const maxDistance = options . maxDistance ? ? 220 ;
const preferFar = options . preferFar === true ;
const rows = [ ] ;
const seen = new Set ( ) ;
for ( const target of civicTransportTargets ) {
if ( target === origin || componentAt ( target ) !== component ) continue ;
const key = ` ${ Math . round ( target . x ) } , ${ Math . round ( target . y ) } ` ;
if ( seen . has ( key ) ) continue ;
seen . add ( key ) ;
const d = Math . hypot ( target . x - origin . x , target . y - origin . y ) ;
if ( d < minDistance || d > maxDistance ) continue ;
const pop = Number ( target . population || target . municipalityPopulation || 0 ) ;
const importance = Math . log1p ( Math . max ( 0 , pop ) ) * 2.1 + ( target . isPrefecturalCapital ? 8 : 0 ) + ( target . isRegionalCapital ? 6 : 0 ) + ( target . portClass === "major" ? 4 : 0 ) ;
const score = preferFar ? d * 0.34 + importance : d - importance * 0.55 ;
rows . push ( { ... target , d , score } ) ;
}
rows . sort ( ( a , b ) => a . score - b . score || b . d - a . d ) ;
return rows . slice ( 0 , options . limit ? ? 16 ) ;
}
function remoteLandTargetOnComponent ( origin , options = { } ) {
const component = componentAt ( origin ) ;
if ( component < 0 ) return null ;
const minDistance = options . minDistance ? ? 18 ;
const maxDistance = options . maxDistance ? ? 170 ;
let best = null ;
// Coarse scan is enough to choose an OD anchor; the emitted route is still
// solved at full raster resolution by the production terrain router.
for ( let y = 1 ; y < MAP _H - 1 ; y += 3 ) for ( let x = 1 ; x < MAP _W - 1 ; x += 3 ) {
const idx = indexOf ( x , y ) ;
if ( landComponentId [ idx ] !== component || terrain ? . sea ? . [ idx ] ) continue ;
const d = Math . hypot ( x - origin . x , y - origin . y ) ;
if ( d < minDistance || d > maxDistance ) continue ;
const slopePenalty = ( terrain ? . slope ? . [ idx ] || 0 ) * 24 ;
const ridgePenalty = ( terrain ? . ridgeField ? . [ idx ] || 0 ) * 18 ;
const elevationPenalty = Math . max ( 0 , ( terrain ? . elevation ? . [ idx ] || 0 ) - 0.68 ) * 30 ;
// Prefer a meaningful regional extension, but avoid selecting a mountain
// summit merely because it is the farthest point on the component.
const score = d - slopePenalty - ridgePenalty - elevationPenalty ;
if ( ! best || score > best . score ) best = { x , y , d , score , remoteLandAnchor : true } ;
}
return best ;
}
const pathHasComponent = ( paths , component ) => {
if ( component < 0 ) return false ;
for ( const path of paths || [ ] ) for ( const [ x , y ] of path || [ ] ) if ( inside ( x , y ) && landComponentId [ indexOf ( x , y ) ] === component ) return true ;
return false ;
} ;
debug . landComponentCount = landComponentCount ;
2026-05-28 00:30:09 +09:00
// Local roads after admin: every municipal office cell should lie on a road.
const settlementTargets = [ ... ( features . modernCities || [ ] ) , ... ( features . markets || [ ] ) , ... ( features . villages || [ ] ) , ... ( features . ports || [ ] ) ] ;
for ( const center of adminCenters || [ ] ) {
if ( ! center || ! inside ( center . x , center . y ) ) continue ;
debug . adminCentersChecked ++ ;
const roadSet = [ ... minorRoads , ... nationalRoads , ... externalRoads ] ;
if ( anyPathTouches ( roadSet , center , 0.65 ) ) continue ;
const nearRoad = nearestPointOnPaths ( roadSet , center , 22 ) ;
const nearSettlement = nearestEntity ( settlementTargets , center , 18 ) ;
const target = nearRoad || nearSettlement ;
2026-08-11 21:51:07 +09:00
let path = [ ] ;
if ( target ) {
const d = Math . hypot ( target . x - center . x , target . y - center . y ) ;
path = terrainFirstConnector ( center , target , terrain , {
maxLength : Math . max ( 18 , d * 2.8 + 20 ) , maxSeaRun : 0 , maxTunnelRun : 6 ,
maxExpanded : Math . min ( SIZE , Math . max ( 9000 , Math . floor ( d * d * 8 + 5000 ) ) ) , maxElevation : 0.82 ,
} ) ;
2026-05-28 00:30:09 +09:00
}
if ( path . length >= 2 && pathTouchesCell ( path , center . x , center . y , 0.65 ) ) {
minorRoads . push ( path ) ;
debug . adminLocalRoadsAdded ++ ;
}
}
// National roads after admin/settlements: try to cover red-dot towns by chain routes instead of one spur per town.
2026-08-11 21:51:07 +09:00
function concatPaths ( parts , maxJoinGap = 2.25 ) {
const valid = ( parts || [ ] ) . filter ( ( part ) => Array . isArray ( part ) && part . length >= 2 ) ;
if ( ! valid . length ) return [ ] ;
const out = valid [ 0 ] . map ( ( pt ) => [ pt [ 0 ] , pt [ 1 ] ] ) ;
for ( let partIndex = 1 ; partIndex < valid . length ; partIndex ++ ) {
let part = valid [ partIndex ] ;
const tail = out [ out . length - 1 ] ;
const dForward = Math . hypot ( tail [ 0 ] - part [ 0 ] [ 0 ] , tail [ 1 ] - part [ 0 ] [ 1 ] ) ;
const pLast = part [ part . length - 1 ] ;
const dReverse = Math . hypot ( tail [ 0 ] - pLast [ 0 ] , tail [ 1 ] - pLast [ 1 ] ) ;
if ( dReverse < dForward ) part = [ ... part ] . reverse ( ) ;
const head = part [ 0 ] ;
const gap = Math . hypot ( tail [ 0 ] - head [ 0 ] , tail [ 1 ] - head [ 1 ] ) ;
// A failed intermediate A* leg used to be silently omitted and the next
// successful leg was appended anyway. Canvas then drew the missing tens
// of cells as one perfectly straight segment. A production trunk chain is
// atomic: if any leg is not contiguous, reject the whole chain so a later
// terrain-routed service pass can rebuild it correctly.
if ( gap > maxJoinGap ) return [ ] ;
2026-05-28 00:30:09 +09:00
for ( const pt of part ) {
2026-08-11 21:51:07 +09:00
if ( ! out . length || out [ out . length - 1 ] [ 0 ] !== pt [ 0 ] || out [ out . length - 1 ] [ 1 ] !== pt [ 1 ] ) out . push ( [ pt [ 0 ] , pt [ 1 ] ] ) ;
2026-05-28 00:30:09 +09:00
}
}
return out ;
}
function townWeight ( p ) {
return ( p . population || 0 ) + ( p . isPrefecturalCapital ? 800000 : 0 ) + ( p . isRegionalCapital ? 350000 : 0 ) + ( p . portClass === "major" ? 220000 : p . portClass === "regional" ? 120000 : p . portClass === "fishing" ? 45000 : 0 ) ;
}
2026-05-28 15:48:42 +09:00
function relayGeometryAcceptable ( points , options = { } ) {
const pts = ( points || [ ] ) . filter ( ( p ) => p && Number . isFinite ( p . x ) && Number . isFinite ( p . y ) ) ;
if ( pts . length < 3 ) return true ;
const first = pts [ 0 ] ;
const last = pts [ pts . length - 1 ] ;
const vx = last . x - first . x ;
const vy = last . y - first . y ;
const direct = Math . hypot ( vx , vy ) ;
if ( direct < 0.001 ) return false ;
let via = 0 ;
for ( let i = 1 ; i < pts . length ; i ++ ) via += Math . hypot ( pts [ i ] . x - pts [ i - 1 ] . x , pts [ i ] . y - pts [ i - 1 ] . y ) ;
const maxDetour = options . maxDetour ? ? 1.68 ;
if ( via > direct * maxDetour + ( options . detourSlack ? ? 16 ) ) return false ;
const maxOffset = Math . max ( options . minOffset ? ? 14 , Math . min ( options . maxOffset ? ? 30 , direct * ( options . offsetRatio ? ? 0.32 ) ) ) ;
for ( let i = 1 ; i < pts . length - 1 ; i ++ ) {
const p = pts [ i ] ;
const wx = p . x - first . x ;
const wy = p . y - first . y ;
const t = ( wx * vx + wy * vy ) / Math . max ( 0.0001 , direct * direct ) ;
const projX = first . x + vx * t ;
const projY = first . y + vy * t ;
const offset = Math . hypot ( p . x - projX , p . y - projY ) ;
if ( t < ( options . minProjection ? ? - 0.10 ) || t > ( options . maxProjection ? ? 1.10 ) ) return false ;
if ( offset > maxOffset ) return false ;
}
return true ;
}
function pathGeometryAcceptable ( path , options = { } ) {
if ( ! path || path . length < 3 ) return true ;
const step = Math . max ( 1 , Math . floor ( path . length / 10 ) ) ;
const pts = [ ] ;
for ( let k = 0 ; k < path . length ; k += step ) pts . push ( { x : path [ k ] [ 0 ] , y : path [ k ] [ 1 ] } ) ;
const last = path [ path . length - 1 ] ;
pts . push ( { x : last [ 0 ] , y : last [ 1 ] } ) ;
return relayGeometryAcceptable ( pts , options ) ;
}
2026-05-28 00:30:09 +09:00
function nearestTrunkOrHub ( p , maxDistance = 85 ) {
const trunk = nearestPointOnPaths ( [ ... nationalRoads , ... externalRoads ] , p , maxDistance ) ;
if ( trunk ) return trunk ;
return nearestEntity ( [ ... ( features . modernCities || [ ] ) , ... ( features . ports || [ ] ) , ... ( features . markets || [ ] ) , ... ( features . externalGateways || [ ] ) ] , p , maxDistance ) ;
}
function buildTownChain ( start , pool , maxHops = 7 ) {
const chain = [ start ] ;
let cur = start ;
for ( let hop = 1 ; hop < maxHops ; hop ++ ) {
let best = null ;
for ( const town of pool ) {
if ( chain . includes ( town ) ) continue ;
const d = Math . hypot ( cur . x - town . x , cur . y - town . y ) ;
if ( d > 42 ) continue ;
const score = d - Math . min ( 18 , Math . sqrt ( Math . max ( 0 , townWeight ( town ) ) ) / 70 ) ;
if ( ! best || score < best . score ) best = { town , d , score } ;
}
if ( ! best ) break ;
chain . push ( best . town ) ;
cur = best . town ;
}
return chain ;
}
function addNationalTownChains ( ) {
let uncovered = townsForNational
. filter ( ( town ) => town && inside ( town . x , town . y ) && ! anyPathTouches ( [ ... nationalRoads , ... externalRoads ] , town , 0.65 ) )
. sort ( ( a , b ) => townWeight ( b ) - townWeight ( a ) ) ;
let chainsAdded = 0 ;
let townsCovered = 0 ;
while ( uncovered . length ) {
const start = uncovered . shift ( ) ;
2026-05-28 15:48:42 +09:00
let chain = buildTownChain ( start , uncovered , 7 ) ;
2026-05-28 00:30:09 +09:00
uncovered = uncovered . filter ( ( town ) => ! chain . includes ( town ) ) ;
const parts = [ ] ;
2026-05-28 15:48:42 +09:00
let before = nearestTrunkOrHub ( chain [ 0 ] , 80 ) ;
let after = chain . length >= 2 ? nearestTrunkOrHub ( chain [ chain . length - 1 ] , 80 ) : null ;
const relayPoints = [ before || chain [ 0 ] , ... chain , after || chain [ chain . length - 1 ] ] ;
if ( ! relayGeometryAcceptable ( relayPoints , { maxDetour : 1.62 , minOffset : 12 , maxOffset : 26 , offsetRatio : 0.30 } ) ) {
// The town-chain pass is a coverage fallback, not a mandate to drag a
// road through a remote off-axis waypoint. Collapse to a single spur
// when the waypoint chain would create a hooked or S-shaped route.
chain = [ chain [ 0 ] ] ;
before = nearestTrunkOrHub ( chain [ 0 ] , 80 ) ;
after = null ;
}
2026-05-28 00:30:09 +09:00
if ( before ) {
2026-08-11 21:51:07 +09:00
const p = terrainFirstConnector ( before , chain [ 0 ] , terrain , { maxLength : 90 , maxSeaRun : 0 , maxTunnelRun : 10 , shortFallback : 8 } ) ;
2026-05-28 00:30:09 +09:00
if ( p . length ) parts . push ( p ) ;
}
for ( let i = 1 ; i < chain . length ; i ++ ) {
const d = Math . hypot ( chain [ i - 1 ] . x - chain [ i ] . x , chain [ i - 1 ] . y - chain [ i ] . y ) ;
2026-05-28 15:48:42 +09:00
const segmentPoints = [ chain [ i - 1 ] , chain [ i ] ] ;
if ( ! relayGeometryAcceptable ( segmentPoints , { maxDetour : 1.25 , minOffset : 10 , maxOffset : 18 } ) ) continue ;
2026-08-11 21:51:07 +09:00
const p = terrainFirstConnector ( chain [ i - 1 ] , chain [ i ] , terrain , { maxLength : Math . max ( 32 , d * 2.45 + 22 ) , maxSeaRun : 0 , maxTunnelRun : 10 , shortFallback : 8 } ) ;
2026-05-28 00:30:09 +09:00
if ( p . length ) parts . push ( p ) ;
}
if ( after ) {
2026-08-11 21:51:07 +09:00
const p = terrainFirstConnector ( chain [ chain . length - 1 ] , after , terrain , { maxLength : 90 , maxSeaRun : 0 , maxTunnelRun : 10 , shortFallback : 8 } ) ;
2026-05-28 00:30:09 +09:00
if ( p . length ) parts . push ( p ) ;
}
let path = concatPaths ( parts ) ;
if ( path . length < 2 ) {
const target = nearestTrunkOrHub ( chain [ 0 ] , 90 ) ;
2026-08-11 21:51:07 +09:00
path = target ? terrainFirstConnector ( chain [ 0 ] , target , terrain , { maxLength : 110 , maxSeaRun : 0 , maxTunnelRun : 10 , shortFallback : 8 } ) : [ ] ;
2026-05-28 00:30:09 +09:00
}
2026-05-28 15:48:42 +09:00
if ( path . length >= 2 && pathGeometryAcceptable ( path , { maxDetour : 1.78 , minOffset : 14 , maxOffset : 32 , offsetRatio : 0.34 } ) && chain . some ( ( town ) => pathTouchesCell ( path , town . x , town . y , 0.65 ) ) ) {
2026-05-28 00:30:09 +09:00
nationalRoads . push ( path ) ;
chainsAdded ++ ;
townsCovered += chain . filter ( ( town ) => pathTouchesCell ( path , town . x , town . y , 0.65 ) ) . length ;
}
}
return { chainsAdded , townsCovered } ;
}
const chainDebug = addNationalTownChains ( ) ;
debug . nationalTownChainsAdded = chainDebug . chainsAdded ;
debug . nationalTownChainTownsCovered = chainDebug . townsCovered ;
debug . nationalTownSpursAdded = chainDebug . chainsAdded ;
2026-08-11 21:51:07 +09:00
function routeAlongTerrainGuide ( start , guidePath , mode = "national" , options = { } ) {
if ( ! start || ! guidePath ? . length ) return [ ] ;
let nearestIndex = - 1 , nearestDistance = Infinity ;
for ( let k = 0 ; k < guidePath . length ; k ++ ) {
const q = guidePath [ k ] ;
const d = Math . hypot ( q [ 0 ] - start . x , q [ 1 ] - start . y ) ;
if ( d < nearestDistance ) { nearestDistance = d ; nearestIndex = k ; }
}
if ( nearestIndex < 0 ) return [ ] ;
const lengthToward = ( dir ) => {
let len = 0 ;
for ( let k = nearestIndex + dir ; k >= 0 && k < guidePath . length ; k += dir ) {
const prev = guidePath [ k - dir ] ;
len += Math . hypot ( guidePath [ k ] [ 0 ] - prev [ 0 ] , guidePath [ k ] [ 1 ] - prev [ 1 ] ) ;
}
return len ;
} ;
const dir = lengthToward ( 1 ) >= lengthToward ( - 1 ) ? 1 : - 1 ;
const desired = options . desiredLength ? ? 64 ;
const stepDistance = options . stepDistance ? ? 10 ;
const guideTargets = [ ] ;
let walked = 0 , nextSample = Math . max ( 6 , stepDistance ) ;
let last = guidePath [ nearestIndex ] ;
for ( let k = nearestIndex + dir ; k >= 0 && k < guidePath . length && walked <= desired + stepDistance ; k += dir ) {
const q = guidePath [ k ] ;
walked += Math . hypot ( q [ 0 ] - last [ 0 ] , q [ 1 ] - last [ 1 ] ) ;
last = q ;
if ( walked >= nextSample ) {
guideTargets . push ( { x : q [ 0 ] , y : q [ 1 ] } ) ;
nextSample += stepDistance ;
}
}
if ( ! guideTargets . length ) return [ ] ;
const parts = [ ] ;
let cur = { x : start . x , y : start . y } ;
for ( const target of guideTargets ) {
const d = Math . hypot ( target . x - cur . x , target . y - cur . y ) ;
if ( d < 1.5 ) continue ;
let leg = routeTerrainPath ( cur , target , terrain , {
maxLength : d * 3.4 + 24 , maxSeaRun : 0 , maxTunnelRun : 0 ,
snapRadius : 0.8 , maxExpanded : SIZE , maxElevation : 0.695 , strictTerrain : true ,
} ) ;
if ( ! leg . length ) leg = routeLandConnectedTerrainFallback ( cur , target , terrain , {
maxLength : Math . min ( SIZE , d * 4.2 + 36 ) , maxElevation : 0.695 , strictTerrain : true ,
} ) ;
if ( leg . length < 2 ) return [ ] ;
parts . push ( leg ) ;
const tail = leg [ leg . length - 1 ] ;
cur = { x : tail [ 0 ] , y : tail [ 1 ] } ;
}
const joined = concatPaths ( parts , 2.25 ) ;
if ( joined . length < 4 ) return [ ] ;
const smoothed = terrainSafeSmooth ( joined , terrain , mode , mode === "expressway" ? 3 : 2 ) ;
return hardTerrainPathValid ( smoothed , mode ) ? smoothed : [ ] ;
}
function sharedTerrainAlignmentFromGuide ( start , guidePath , mode = "national" , options = { } ) {
if ( ! start || ! guidePath ? . length ) return [ ] ;
let nearestIndex = - 1 , nearestDistance = Infinity ;
for ( let k = 0 ; k < guidePath . length ; k ++ ) {
const q = guidePath [ k ] ;
const d = Math . hypot ( q [ 0 ] - start . x , q [ 1 ] - start . y ) ;
if ( d < nearestDistance ) { nearestDistance = d ; nearestIndex = k ; }
}
if ( nearestIndex < 0 ) return [ ] ;
const branchLength = ( dir ) => {
let len = 0 , last = guidePath [ nearestIndex ] ;
for ( let k = nearestIndex + dir ; k >= 0 && k < guidePath . length ; k += dir ) {
const q = guidePath [ k ] ; len += Math . hypot ( q [ 0 ] - last [ 0 ] , q [ 1 ] - last [ 1 ] ) ; last = q ;
}
return len ;
} ;
const dir = branchLength ( 1 ) >= branchLength ( - 1 ) ? 1 : - 1 ;
const join = { x : guidePath [ nearestIndex ] [ 0 ] , y : guidePath [ nearestIndex ] [ 1 ] } ;
let connector = [ ] ;
if ( nearestDistance > 0.75 ) {
connector = routeTerrainPath ( start , join , terrain , {
maxLength : nearestDistance * 3.6 + 28 , maxSeaRun : 0 , maxTunnelRun : 0 ,
snapRadius : 0.7 , maxExpanded : SIZE , maxElevation : 0.695 , strictTerrain : true ,
} ) ;
if ( ! connector . length ) connector = routeLandConnectedTerrainFallback ( start , join , terrain , {
maxLength : Math . min ( SIZE , nearestDistance * 4.5 + 40 ) , maxElevation : 0.695 , strictTerrain : true ,
} ) ;
if ( connector . length < 2 ) return [ ] ;
} else connector = [ [ Math . round ( start . x ) , Math . round ( start . y ) ] , [ join . x , join . y ] ] ;
const desired = options . desiredLength ? ? 58 ;
const slice = [ [ join . x , join . y ] ] ;
let walked = 0 , last = guidePath [ nearestIndex ] ;
for ( let k = nearestIndex + dir ; k >= 0 && k < guidePath . length && walked < desired ; k += dir ) {
const q = guidePath [ k ] ;
walked += Math . hypot ( q [ 0 ] - last [ 0 ] , q [ 1 ] - last [ 1 ] ) ;
slice . push ( [ q [ 0 ] , q [ 1 ] ] ) ; last = q ;
}
if ( walked < Math . min ( 14 , desired * 0.35 ) ) return [ ] ;
const joined = concatPaths ( [ connector , slice ] , 2.25 ) ;
if ( joined . length < 4 ) return [ ] ;
// Do not geometrically smooth the shared section: the guide already passed
// the production terrain invariant, and corner-cutting could reintroduce a
// mountain/sea shortcut. Exact shared alignment is preferable to a fake
// parallel motorway when no independent corridor exists.
return hardTerrainPathValid ( joined , mode ) ? joined : [ ] ;
}
function sharedSuburbanTerrainAlignment ( city , guidePath , mode = "expressway" , options = { } ) {
if ( ! city || ! guidePath ? . length ) return [ ] ;
const inner = Math . max ( 7 , ( city . coreRadius || 4 ) + 4.5 ) ;
const outer = Math . max ( inner + 6 , Math . min ( 34 , ( city . urbanRadius || 12 ) * 2.15 ) ) ;
const desired = options . desiredLength ? ? 64 ;
const rect = options . focusRect || null ;
const inRect = ( q ) => ! rect || ( q [ 0 ] >= rect . x0 + 1 && q [ 1 ] >= rect . y0 + 1 && q [ 0 ] < rect . x1 - 1 && q [ 1 ] < rect . y1 - 1 ) ;
let best = null ;
for ( let k = 0 ; k < guidePath . length ; k ++ ) {
const q = guidePath [ k ] ;
const cityD = Math . hypot ( q [ 0 ] - city . x , q [ 1 ] - city . y ) ;
if ( cityD < inner || cityD > outer ) continue ;
for ( const dir of [ - 1 , 1 ] ) {
const slice = [ [ q [ 0 ] , q [ 1 ] ] ] ;
let len = 0 , insideLen = 0 , last = q ;
for ( let j = k + dir ; j >= 0 && j < guidePath . length && len < desired ; j += dir ) {
const r = guidePath [ j ] ;
const step = Math . hypot ( r [ 0 ] - last [ 0 ] , r [ 1 ] - last [ 1 ] ) ;
len += step ;
if ( inRect ( r ) ) insideLen += step ;
slice . push ( [ r [ 0 ] , r [ 1 ] ] ) ; last = r ;
}
if ( len < Math . min ( 14 , desired * 0.30 ) ) continue ;
const score = insideLen * 3 + len - Math . abs ( cityD - ( inner + outer ) * 0.5 ) * 0.2 ;
if ( ! best || score > best . score ) best = { slice , score } ;
}
}
if ( ! best || ! hardTerrainPathValid ( best . slice , mode ) ) return [ ] ;
return best . slice ;
}
2026-05-28 15:48:42 +09:00
function suburbanExpresswayAnchorForCity ( city , target = null ) {
if ( ! city || ! inside ( city . x , city . y ) ) return null ;
const sea = terrain ? . sea ;
const elevation = terrain ? . elevation ;
const slope = terrain ? . slope ;
const ridgeField = terrain ? . ridgeField ;
const inner = Math . max ( 8 , Math . round ( ( city . coreRadius || 4 ) + 6 ) ) ;
const outer = Math . max ( inner + 7 , Math . round ( ( city . urbanRadius || 12 ) * 1.9 ) ) ;
let best = null ;
for ( let dy = - outer ; dy <= outer ; dy ++ ) {
for ( let dx = - outer ; dx <= outer ; dx ++ ) {
const x = city . x + dx , y = city . y + dy ;
if ( ! inside ( x , y ) ) continue ;
const d = Math . hypot ( dx , dy ) ;
if ( d < inner || d > outer ) continue ;
const i = indexOf ( x , y ) ;
if ( sea ? . [ i ] ) continue ;
2026-08-11 21:51:07 +09:00
if ( landComponentId [ i ] !== componentAt ( city ) ) continue ;
2026-05-28 15:48:42 +09:00
const radial = Math . abs ( d - ( inner + outer ) * 0.52 ) ;
const targetBias = target ? Math . hypot ( x - target . x , y - target . y ) * 0.038 : 0 ;
const score = - radial * 0.26 - targetBias - ( slope ? . [ i ] || 0 ) * 0.70 - ( ridgeField ? . [ i ] || 0 ) * 0.55 - Math . max ( 0 , ( elevation ? . [ i ] || 0 ) - 0.70 ) * 0.75 ;
if ( ! best || score > best . score ) best = { x , y , score } ;
}
}
return best ;
}
function suburbanExpresswayStubForCity ( city , preferredAnchor = null ) {
if ( ! city || ! inside ( city . x , city . y ) ) return [ ] ;
const angles = [ ] ;
if ( preferredAnchor ) angles . push ( Math . atan2 ( preferredAnchor . y - city . y , preferredAnchor . x - city . x ) ) ;
for ( let k = 0 ; k < 8 ; k ++ ) angles . push ( ( Math . PI * 2 * k ) / 8 + ( k % 2 ? 0.18 : 0 ) ) ;
const seenAngles = new Set ( ) ;
for ( const angle of angles ) {
const bucket = Math . round ( angle * 100 ) / 100 ;
if ( seenAngles . has ( bucket ) ) continue ;
seenAngles . add ( bucket ) ;
const hint = { x : Math . round ( city . x + Math . cos ( angle ) * 120 ) , y : Math . round ( city . y + Math . sin ( angle ) * 120 ) } ;
const anchor = suburbanExpresswayAnchorForCity ( city , hint ) ;
if ( ! anchor ) continue ;
const minD = Math . max ( 20 , ( city . urbanRadius || 12 ) * 1.35 ) ;
const maxD = Math . max ( minD + 10 , ( city . urbanRadius || 12 ) * 2.65 ) ;
let bestEnd = null ;
for ( let d = minD ; d <= maxD ; d += 2 ) {
const x = Math . round ( city . x + Math . cos ( angle ) * d ) ;
const y = Math . round ( city . y + Math . sin ( angle ) * d ) ;
if ( ! inside ( x , y ) ) continue ;
const i = indexOf ( x , y ) ;
if ( terrain ? . sea ? . [ i ] ) continue ;
bestEnd = { x , y } ;
}
if ( ! bestEnd || Math . hypot ( bestEnd . x - anchor . x , bestEnd . y - anchor . y ) < 8 ) continue ;
const d = Math . hypot ( bestEnd . x - anchor . x , bestEnd . y - anchor . y ) ;
2026-08-11 21:51:07 +09:00
let path = terrainFirstConnector ( anchor , bestEnd , terrain , { maxLength : d * 2.7 + 30 , maxSeaRun : 0 , maxTunnelRun : 10 , shortFallback : 7 } ) ;
2026-05-28 15:48:42 +09:00
if ( ! path . length ) path = routeTerrainPath ( anchor , bestEnd , terrain , { maxLength : d * 2.6 + 20 , maxSeaRun : 0 , maxTunnelRun : 10 , snapRadius : 1.6 } ) ;
if ( path . length >= 4 ) return path ;
}
return [ ] ;
}
function expresswayServesCityFringe ( city ) {
const inner = Math . max ( 7.0 , ( city . coreRadius || 4 ) + 4.5 ) ;
const outer = Math . max ( inner + 7 , ( city . urbanRadius || 12 ) * 2.0 ) ;
for ( const path of [ ... ( features . expressways || [ ] ) , ... ( features . externalExpressways || [ ] ) ] ) {
let inBand = false ;
2026-08-11 21:51:07 +09:00
let nearOuter = false ;
let minD = Infinity ;
let maxD = 0 ;
2026-05-28 15:48:42 +09:00
for ( const [ x , y ] of path || [ ] ) {
const d = Math . hypot ( x - city . x , y - city . y ) ;
2026-08-11 21:51:07 +09:00
minD = Math . min ( minD , d ) ;
maxD = Math . max ( maxD , d ) ;
2026-05-28 15:48:42 +09:00
if ( d >= inner && d <= outer ) inBand = true ;
2026-08-11 21:51:07 +09:00
if ( d >= outer - 2 ) nearOuter = true ;
}
const serviceRadius = Math . max ( 18 , Math . min ( 34 , ( city . urbanRadius || 12 ) * 2.2 ) ) ;
const largeCity = ( city . population || 0 ) >= 180000 ;
const largeCityReach = Math . max ( 12 , Math . min ( 24 , ( city . urbanRadius || 12 ) * 1.55 ) ) ;
// For a several-hundred-thousand-person city, the broad annulus rule is
// deliberately disabled: remote fringe passage is not city service.
if ( largeCity ) {
if ( minD <= largeCityReach && pathLengthCells ( path ) >= 12 && maxD - minD >= 8 ) return true ;
} else {
// Smaller cities may be served by a regional bypass traversing their
// suburban annulus even if it stays farther from the urban core.
if ( inBand && ( nearOuter || maxD - minD >= 8 ) ) return true ;
if ( minD <= serviceRadius && pathLengthCells ( path ) >= 10 && maxD - minD >= 7 ) return true ;
2026-05-28 15:48:42 +09:00
}
}
return false ;
}
2026-08-11 21:51:07 +09:00
function ensureMajorCityExpresswayLinks ( minPopulation = 60000 ) {
2026-05-28 02:47:33 +09:00
const cities = ( features . modernCities || [ ] )
2026-08-11 21:51:07 +09:00
. filter ( ( city ) => city && inside ( city . x , city . y ) && ! terrain ? . sea ? . [ indexOf ( city . x , city . y ) ] && ( ( city . population || 0 ) >= minPopulation || city . isPrefecturalCapital || city . isRegionalCapital ) )
2026-05-28 02:47:33 +09:00
. sort ( ( a , b ) => ( b . population || 0 ) - ( a . population || 0 ) ) ;
2026-08-11 21:51:07 +09:00
const result = { minPopulation , checked : cities . length , covered : 0 , added : 0 , noTarget : 0 , noPath : 0 , seededBackbone : 0 , seededComponentBackbones : 0 } ;
2026-05-28 15:48:42 +09:00
features . expressways || = [ ] ;
if ( ! cities . length ) return result ;
2026-08-11 21:51:07 +09:00
if ( ! ( features . expressways || [ ] ) . length && ! ( features . externalExpressways || [ ] ) . length && cities . length >= 2 ) {
const a = suburbanExpresswayAnchorForCity ( cities [ 0 ] , cities [ 1 ] ) ;
const b = suburbanExpresswayAnchorForCity ( cities [ 1 ] , cities [ 0 ] ) ;
if ( a && b ) {
const d = Math . hypot ( a . x - b . x , a . y - b . y ) ;
let seedPath = routeTerrainPath ( a , b , terrain , { maxLength : d * 2.8 + 70 , maxSeaRun : 0 , maxTunnelRun : 10 , snapRadius : 2.5 , maxExpanded : SIZE } ) ;
if ( ! seedPath . length ) seedPath = terrainFirstConnector ( a , b , terrain , { skipPrimaryRoute : true , maxLength : d * 2.9 + 72 , maxSeaRun : 0 , maxTunnelRun : 10 , shortFallback : 7 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( seedPath . length >= 4 && trunkElevationSafe ( seedPath , terrain , 0.72 ) ) {
seedPath = terrainSafeSmooth ( seedPath , terrain , "expressway" , 4 ) ;
if ( hardTerrainPathValid ( seedPath , "expressway" ) ) {
features . expressways . push ( seedPath ) ;
result . seededBackbone ++ ;
}
}
}
}
// A global expressway can exist on another island while a sizeable land
// component containing several major cities has no motorway at all. Seed a
// terrain-aware backbone independently for each such component. This is not
// a straight fallback: both suburban anchors and the route stay on land.
const citiesByComponent = new Map ( ) ;
for ( const city of cities ) {
const component = componentAt ( city ) ;
if ( component < 0 ) continue ;
if ( ! citiesByComponent . has ( component ) ) citiesByComponent . set ( component , [ ] ) ;
citiesByComponent . get ( component ) . push ( city ) ;
}
for ( const [ component , componentCities ] of citiesByComponent ) {
if ( componentCities . length < 1 ) continue ;
const existingNetwork = [ ... ( features . expressways || [ ] ) , ... ( features . externalExpressways || [ ] ) ] ;
if ( pathHasComponent ( existingNetwork , component ) ) continue ;
const ordered = componentCities . slice ( ) . sort ( ( a , b ) => ( b . population || 0 ) - ( a . population || 0 ) ) ;
let seeded = false ;
for ( let aIndex = 0 ; aIndex < Math . min ( 3 , ordered . length ) && ! seeded ; aIndex ++ ) {
const aCity = ordered [ aIndex ] ;
const targetPool = [
... ordered . filter ( ( q ) => q !== aCity ) ,
... sameComponentCivicTargets ( aCity , { minDistance : 16 , maxDistance : 230 , limit : 20 , preferFar : componentCities . length < 2 } ) ,
] ;
const targetSeen = new Set ( ) ;
const targets = targetPool . filter ( ( q ) => {
if ( ! q || componentAt ( q ) !== component ) return false ;
const key = ` ${ Math . round ( q . x ) } , ${ Math . round ( q . y ) } ` ;
if ( key === ` ${ Math . round ( aCity . x ) } , ${ Math . round ( aCity . y ) } ` || targetSeen . has ( key ) ) return false ;
targetSeen . add ( key ) ;
return Math . hypot ( q . x - aCity . x , q . y - aCity . y ) >= 12 ;
} ) . sort ( ( p , q ) => {
const pd = Math . hypot ( p . x - aCity . x , p . y - aCity . y ) ;
const qd = Math . hypot ( q . x - aCity . x , q . y - aCity . y ) ;
// Single-major-city components need a meaningful regional corridor,
// not a tiny motorway stub. Prefer a farther populated/administrative
// anchor while staying on the same land component.
return componentCities . length < 2 ? qd - pd : pd - qd ;
} ) ;
for ( const bCity of targets . slice ( 0 , 18 ) ) {
const a = suburbanExpresswayAnchorForCity ( aCity , bCity ) ;
const b = suburbanExpresswayAnchorForCity ( bCity , aCity ) ;
let seedPath = [ ] ;
if ( a && b && componentAt ( a ) === component && componentAt ( b ) === component ) {
const d = Math . hypot ( a . x - b . x , a . y - b . y ) ;
if ( d >= 7 ) {
seedPath = routeTerrainPath ( a , b , terrain , { maxLength : d * 3.1 + 90 , maxSeaRun : 0 , maxTunnelRun : 28 , snapRadius : 2.5 , maxExpanded : SIZE } ) ;
if ( ! seedPath . length ) seedPath = terrainFirstConnector ( a , b , terrain , { skipPrimaryRoute : true , maxLength : d * 3.2 + 94 , maxSeaRun : 0 , maxTunnelRun : 28 , shortFallback : 7 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! seedPath . length ) seedPath = routeLandConnectedTerrainFallback ( a , b , terrain , { maxLength : SIZE , maxElevation : 0.695 } ) ;
}
}
// If ring anchors are unavailable on a narrow/irregular island, route
// city-to-city on the same land component and crop the core-city ends.
// The emitted motorway still starts outside each urban core and never
// becomes a terrain-ignoring straight connector.
if ( ! seedPath . length ) {
let corePath = routeLandConnectedTerrainFallback ( aCity , bCity , terrain , { maxLength : SIZE } ) ;
if ( corePath . length >= 8 ) {
const aInner = Math . max ( 7.0 , ( aCity . coreRadius || 4 ) + 4.5 ) ;
const bInner = Math . max ( 7.0 , ( bCity . coreRadius || 4 ) + 4.5 ) ;
let first = corePath . findIndex ( ( [ x , y ] ) => Math . hypot ( x - aCity . x , y - aCity . y ) >= aInner ) ;
let last = - 1 ;
for ( let k = corePath . length - 1 ; k >= 0 ; k -- ) {
const [ x , y ] = corePath [ k ] ;
if ( Math . hypot ( x - bCity . x , y - bCity . y ) >= bInner ) { last = k ; break ; }
}
if ( first >= 0 && last > first + 3 ) corePath = corePath . slice ( first , last + 1 ) ;
else corePath = [ ] ;
}
seedPath = corePath ;
}
if ( seedPath . length < 4 || ! trunkElevationSafe ( seedPath , terrain , 0.72 ) ) continue ;
seedPath = terrainSafeSmooth ( seedPath , terrain , "expressway" , 4 ) ;
const turns = pathSharpTurnStats ( seedPath ) ;
if ( turns . consecutiveExtreme >= 2 || turns . sharpShare > 0.46 ) continue ;
if ( ! hardTerrainPathValid ( seedPath , "expressway" ) ) continue ;
features . expressways . push ( seedPath ) ;
result . seededComponentBackbones ++ ;
seeded = true ;
break ;
}
}
}
2026-05-28 15:48:42 +09:00
for ( const city of cities ) {
if ( expresswayServesCityFringe ( city ) ) { result . covered ++ ; continue ; }
const existing = [ ... ( features . expressways || [ ] ) , ... ( features . externalExpressways || [ ] ) ] ;
2026-08-11 21:51:07 +09:00
const cityComponent = componentAt ( city ) ;
let targets = nearestPointsOnPaths ( existing , city , Infinity , 24 , 3 ) . filter ( ( target ) => componentAt ( target ) === cityComponent ) . slice ( 0 , 14 ) ;
let connectingToExisting = targets . length > 0 ;
if ( ! targets . length ) {
targets = sameComponentCivicTargets ( city , { minDistance : 14 , maxDistance : 230 , limit : 16 , preferFar : true } ) ;
connectingToExisting = false ;
}
if ( ! targets . length ) {
const remote = remoteLandTargetOnComponent ( city , { minDistance : 16 , maxDistance : 180 } ) ;
if ( remote ) targets = [ remote ] ;
}
if ( ! targets . length ) result . noTarget ++ ;
let path = [ ] ;
for ( const target of targets ) {
const anchor = suburbanExpresswayAnchorForCity ( city , target ) ;
if ( ! anchor ) continue ;
const d = Math . hypot ( anchor . x - target . x , anchor . y - target . y ) ;
if ( d < 4 ) continue ;
let candidate = routeTerrainPath ( anchor , target , terrain , { maxLength : d * 3.35 + 90 , maxSeaRun : 0 , maxTunnelRun : 28 , snapRadius : 2.3 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! candidate . length ) candidate = terrainFirstConnector ( anchor , target , terrain , { skipPrimaryRoute : true , maxLength : d * 3.45 + 94 , maxSeaRun : 0 , maxTunnelRun : 28 , shortFallback : 7 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! candidate . length ) candidate = routeLandConnectedTerrainFallback ( anchor , target , terrain , { maxLength : SIZE , maxElevation : 0.695 } ) ;
if ( candidate . length < 4 || ! trunkElevationSafe ( candidate , terrain , 0.72 ) ) continue ;
if ( connectingToExisting ) candidate = trimRouteAtExistingNetwork ( candidate , existing , 3.0 , 4 ) ;
candidate = terrainSafeSmooth ( candidate , terrain , "expressway" , 4 ) ;
const turns = pathSharpTurnStats ( candidate ) ;
if ( turns . consecutiveExtreme >= 2 || turns . sharpShare > 0.46 ) continue ;
path = candidate ;
break ;
}
// If the nearest existing motorway lies behind an awkward ridge/coast corridor, prefer a
// city-to-city backbone to a straight or terrain-ignoring feeder. One such corridor can
// serve two major cities and therefore also keeps motorway branching lower.
if ( ! path . length ) {
const partnerCities = cities . filter ( ( q ) => q !== city && sameLandComponent ( city , q ) )
. sort ( ( a , b ) => Math . hypot ( a . x - city . x , a . y - city . y ) - Math . hypot ( b . x - city . x , b . y - city . y ) ) ;
for ( const partner of partnerCities . slice ( 0 , 8 ) ) {
let a = suburbanExpresswayAnchorForCity ( city , partner ) ;
let b = suburbanExpresswayAnchorForCity ( partner , city ) ;
let candidate = [ ] ;
if ( a && b && componentAt ( a ) === cityComponent && componentAt ( b ) === cityComponent ) {
const d = Math . hypot ( a . x - b . x , a . y - b . y ) ;
if ( d >= 6 ) {
candidate = routeTerrainPath ( a , b , terrain , { maxLength : d * 3.45 + 108 , maxSeaRun : 0 , maxTunnelRun : 32 , snapRadius : 2.4 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! candidate . length ) candidate = terrainFirstConnector ( a , b , terrain , { skipPrimaryRoute : true , maxLength : d * 3.55 + 112 , maxSeaRun : 0 , maxTunnelRun : 32 , shortFallback : 7 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! candidate . length ) candidate = routeLandConnectedTerrainFallback ( a , b , terrain , { maxLength : SIZE , maxElevation : 0.695 } ) ;
}
}
if ( ! candidate . length ) {
candidate = routeTerrainPath ( city , partner , terrain , { maxLength : Math . hypot ( partner . x - city . x , partner . y - city . y ) * 3.6 + 120 , maxSeaRun : 0 , maxTunnelRun : 34 , snapRadius : 2.2 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! candidate . length ) candidate = routeLandConnectedTerrainFallback ( city , partner , terrain , { maxLength : SIZE , maxElevation : 0.695 } ) ;
if ( candidate . length >= 8 ) {
const cityInner = Math . max ( 7.0 , ( city . coreRadius || 4 ) + 4.5 ) ;
const partnerInner = Math . max ( 7.0 , ( partner . coreRadius || 4 ) + 4.5 ) ;
let first = candidate . findIndex ( ( [ x , y ] ) => Math . hypot ( x - city . x , y - city . y ) >= cityInner ) ;
let last = - 1 ;
for ( let k = candidate . length - 1 ; k >= 0 ; k -- ) {
const [ x , y ] = candidate [ k ] ;
if ( Math . hypot ( x - partner . x , y - partner . y ) >= partnerInner ) { last = k ; break ; }
}
candidate = first >= 0 && last > first + 3 ? candidate . slice ( first , last + 1 ) : [ ] ;
}
}
if ( candidate . length < 4 || ! trunkElevationSafe ( candidate , terrain , 0.72 ) ) continue ;
candidate = terrainSafeSmooth ( candidate , terrain , "expressway" , 4 ) ;
const turns = pathSharpTurnStats ( candidate ) ;
if ( turns . consecutiveExtreme >= 2 || turns . sharpShare > 0.46 ) continue ;
path = candidate ;
break ;
}
}
if ( ! path . length ) {
// If the city already has a terrain-valid national route, use that
// corridor only as a sequence of terrain waypoints. The motorway is
// still independently A*-routed at full resolution; no polyline is
// copied or straight-interpolated from the national road.
const nationalGuides = [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ]
. map ( ( guide ) => ( { guide , hit : nearestPointOnPaths ( [ guide ] , city , 7 ) } ) )
. filter ( ( row ) => row . hit && componentAt ( row . hit ) === cityComponent )
. sort ( ( a , b ) => a . hit . d - b . hit . d ) ;
for ( const row of nationalGuides . slice ( 0 , 4 ) ) {
const hint = row . guide [ Math . min ( row . guide . length - 1 , Math . max ( 0 , Math . floor ( row . guide . length * 0.7 ) ) ) ] || null ;
const targetHint = hint ? { x : hint [ 0 ] , y : hint [ 1 ] } : null ;
const anchor = suburbanExpresswayAnchorForCity ( city , targetHint ) ;
let guided = sharedSuburbanTerrainAlignment ( city , row . guide , "expressway" , { desiredLength : 68 } ) ;
if ( ! guided . length && anchor ) guided = routeAlongTerrainGuide ( anchor , row . guide , "expressway" , { desiredLength : 72 , stepDistance : 9 } ) ;
if ( ! guided . length && anchor ) guided = sharedTerrainAlignmentFromGuide ( anchor , row . guide , "expressway" , { desiredLength : 64 } ) ;
if ( guided . length >= 4 ) { path = guided ; break ; }
}
2026-05-28 15:48:42 +09:00
}
if ( ! path . length ) {
2026-08-11 21:51:07 +09:00
const regionalHint = sameComponentCivicTargets ( city , { minDistance : 12 , maxDistance : 230 , limit : 1 , preferFar : true } ) [ 0 ] || null ;
const stub = suburbanExpresswayStubForCity ( city , regionalHint ) ;
if ( stub . length >= 4 && trunkElevationSafe ( stub , terrain , 0.72 ) ) {
const turns = pathSharpTurnStats ( stub ) ;
if ( turns . consecutiveExtreme < 2 && turns . sharpShare <= 0.46 ) path = terrainSafeSmooth ( stub , terrain , "expressway" , 3 ) ;
}
}
// A candidate can survive the local turn checks but still fail the final
// terrain invariant. Treat that exactly like a routing failure so the
// large-city guarantee below gets a chance to find a valid alternative.
if ( path . length && ( pathLengthCells ( path ) < 3 || ! hardTerrainPathValid ( path , "expressway" ) ) ) {
path = [ ] ;
result . invalidPrimaryCandidateRetried = ( result . invalidPrimaryCandidateRetried || 0 ) + 1 ;
}
if ( ! path . length && ( city . population || 0 ) >= 180000 ) {
// Hard guarantee for large cities on awkward coasts / narrow basins.
// First exploit an already proven terrain corridor (national road or
// railway) only as A* waypoints: the expressway is routed independently
// between suburban points instead of copying the guide geometry.
const largeReach = Math . max ( 12 , Math . min ( 24 , ( city . urbanRadius || 12 ) * 1.55 ) ) ;
const approachInner = Math . max ( 7.0 , ( city . coreRadius || 4 ) + 4.5 ) ;
const terrainGuides = [
... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ,
... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) , ... ( features . externalRailways || [ ] ) ,
] . map ( ( guide ) => ( { guide , hit : nearestPointOnPaths ( [ guide ] , city , largeReach + 4 ) } ) )
. filter ( ( row ) => row . hit && componentAt ( row . hit ) === cityComponent )
. sort ( ( a , b ) => a . hit . d - b . hit . d || b . guide . length - a . guide . length ) ;
result . largeCityGuideRows = ( result . largeCityGuideRows || 0 ) + terrainGuides . length ;
for ( const { guide } of terrainGuides . slice ( 0 , 12 ) ) {
const candidates = [ ] ;
for ( let k = 0 ; k < guide . length ; k ++ ) {
const d = Math . hypot ( guide [ k ] [ 0 ] - city . x , guide [ k ] [ 1 ] - city . y ) ;
if ( d >= approachInner && d <= largeReach ) candidates . push ( k ) ;
}
result . largeCityGuideCandidateStarts = ( result . largeCityGuideCandidateStarts || 0 ) + candidates . length ;
for ( const startIndex of candidates . slice ( 0 , 6 ) ) {
const startPoint = { x : guide [ startIndex ] [ 0 ] , y : guide [ startIndex ] [ 1 ] } ;
const ends = [ 0 , guide . length - 1 ] . sort ( ( a , b ) => Math . abs ( b - startIndex ) - Math . abs ( a - startIndex ) ) ;
for ( const endIndex of ends ) {
const endPoint = { x : guide [ endIndex ] [ 0 ] , y : guide [ endIndex ] [ 1 ] } ;
const d = Math . hypot ( endPoint . x - startPoint . x , endPoint . y - startPoint . y ) ;
if ( d < 10 ) continue ;
let candidate = routeTerrainPath ( startPoint , endPoint , terrain , { maxLength : d * 3.7 + 72 , maxSeaRun : 0 , maxTunnelRun : 30 , snapRadius : 1.2 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! candidate . length ) candidate = routeLandConnectedTerrainFallback ( startPoint , endPoint , terrain , { maxLength : Math . min ( SIZE , d * 4.6 + 96 ) , maxElevation : 0.695 } ) ;
candidate = terrainSafeSmooth ( candidate , terrain , "expressway" , 2 ) ;
if ( candidate . length >= 10 && hardTerrainPathValid ( candidate , "expressway" ) ) {
const turns = pathSharpTurnStats ( candidate ) ;
if ( turns . consecutiveExtreme < 2 && turns . sharpShare <= 0.48 ) {
path = candidate ;
result . largeCityGuideRoutedFallbackAdded = ( result . largeCityGuideRoutedFallbackAdded || 0 ) + 1 ;
break ;
}
}
// At this raster resolution, a national road / railway corridor
// can be the only terrain-valid valley through a mountain block.
// Reuse that already terrain-validated *corridor geometry* rather
// than inventing a straight shortcut or leaving a 200k+ city
// without motorway access. The two modes may share a raster cell
// while remaining distinct transport layers.
let shared = startIndex <= endIndex
? guide . slice ( startIndex , endIndex + 1 )
: guide . slice ( endIndex , startIndex + 1 ) . reverse ( ) ;
const sharedTerrainValid = shared . length >= 10 && hardTerrainPathValid ( shared , "expressway" ) ;
if ( sharedTerrainValid ) {
const turns = pathSharpTurnStats ( shared ) ;
if ( turns . consecutiveExtreme < 2 && turns . sharpShare <= 0.48 ) {
path = shared ;
result . largeCitySharedTerrainCorridorFallbackAdded = ( result . largeCitySharedTerrainCorridorFallbackAdded || 0 ) + 1 ;
break ;
}
}
}
if ( path . length ) break ;
}
if ( path . length ) break ;
}
// If no guide has enough suburban extent, solve the complete route from
// the city at full terrain resolution, then remove only the inner-city
// portion so the motorway still behaves as a suburban approach.
const existingNow = [ ... ( features . expressways || [ ] ) , ... ( features . externalExpressways || [ ] ) ] ;
const directTargets = [
... nearestPointsOnPaths ( existingNow , city , Infinity , 32 , 3 ) . filter ( ( target ) => componentAt ( target ) === cityComponent ) ,
... sameComponentCivicTargets ( city , { minDistance : 22 , maxDistance : 230 , limit : 18 , preferFar : true } ) ,
] ;
const seenDirect = new Set ( ) ;
for ( const target of path . length ? [ ] : directTargets ) {
if ( ! target ) continue ;
const key = ` ${ Math . round ( target . x ) } , ${ Math . round ( target . y ) } ` ;
if ( seenDirect . has ( key ) ) continue ;
seenDirect . add ( key ) ;
const d = Math . hypot ( target . x - city . x , target . y - city . y ) ;
if ( d < 14 ) continue ;
let candidate = routeTerrainPath ( city , target , terrain , { maxLength : d * 3.8 + 132 , maxSeaRun : 0 , maxTunnelRun : 38 , snapRadius : 2.1 , maxExpanded : SIZE , maxElevation : 0.72 } ) ;
if ( ! candidate . length ) candidate = routeLandConnectedTerrainFallback ( city , target , terrain , { maxLength : SIZE , maxElevation : 0.72 } ) ;
if ( candidate . length < 10 ) continue ;
const inner = Math . max ( 7.0 , ( city . coreRadius || 4 ) + 4.5 ) ;
const first = candidate . findIndex ( ( [ x , y ] ) => Math . hypot ( x - city . x , y - city . y ) >= inner ) ;
if ( first < 0 || candidate . length - first < 8 ) continue ;
candidate = candidate . slice ( first ) ;
if ( existingNow . length ) candidate = trimRouteAtExistingNetwork ( candidate , existingNow , 3.0 , 4 ) ;
candidate = terrainSafeSmooth ( candidate , terrain , "expressway" , 3 ) ;
if ( candidate . length < 8 || ! trunkElevationSafe ( candidate , terrain , 0.72 ) || ! hardTerrainPathValid ( candidate , "expressway" ) ) continue ;
const turns = pathSharpTurnStats ( candidate ) ;
if ( turns . consecutiveExtreme >= 2 || turns . sharpShare > 0.48 ) continue ;
path = candidate ;
result . largeCityDirectFallbackAdded = ( result . largeCityDirectFallbackAdded || 0 ) + 1 ;
break ;
}
}
if ( ! path . length || pathLengthCells ( path ) < 3 || ! hardTerrainPathValid ( path , "expressway" ) ) { result . noPath ++ ; continue ; }
features . expressways . push ( path ) ;
2026-05-28 02:47:33 +09:00
result . added ++ ;
}
return result ;
}
function cityRailChain ( minPopulation = 50000 ) {
const cities = ( features . modernCities || [ ] )
. filter ( ( city ) => ( city . population || 0 ) >= minPopulation && inside ( city . x , city . y ) )
. sort ( ( a , b ) => a . x - b . x || a . y - b . y ) ;
const result = { minPopulation , checked : cities . length , chainsAdded : 0 , citiesCovered : 0 } ;
if ( cities . length < 2 ) return result ;
const existingRail = [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) , ... ( features . externalRailways || [ ] ) ] ;
const uncovered = cities . filter ( ( city ) => ! anyPathTouches ( existingRail , city , 1.2 ) ) ;
if ( ! uncovered . length ) return result ;
const remaining = uncovered . slice ( ) ;
let chain = [ remaining . shift ( ) ] ;
while ( remaining . length ) {
const cur = chain [ chain . length - 1 ] ;
let bestIndex = 0 ;
let bestD = Infinity ;
for ( let i = 0 ; i < remaining . length ; i ++ ) {
const d = Math . hypot ( cur . x - remaining [ i ] . x , cur . y - remaining [ i ] . y ) ;
if ( d < bestD ) { bestD = d ; bestIndex = i ; }
}
chain . push ( remaining . splice ( bestIndex , 1 ) [ 0 ] ) ;
}
const parts = [ ] ;
for ( let i = 1 ; i < chain . length ; i ++ ) {
const a = chain [ i - 1 ] , b = chain [ i ] ;
const d = Math . hypot ( a . x - b . x , a . y - b . y ) ;
2026-08-11 21:51:07 +09:00
let p = terrainFirstConnector ( a , b , terrain , { maxLength : d * 2.55 + 30 , maxSeaRun : 0 , maxTunnelRun : 12 , shortFallback : 7 } ) ;
if ( ! p . length ) p = terrainFirstConnector ( a , b , terrain , { maxLength : d * 2.95 + 48 , maxSeaRun : 0 , maxTunnelRun : 14 , shortFallback : 7 } ) ;
2026-05-28 02:47:33 +09:00
if ( p . length ) parts . push ( p ) ;
}
const path = concatPaths ( parts ) ;
if ( path . length >= 2 ) {
features . railways = features . railways || [ ] ;
features . railways . push ( smoothPath ( path , 1 ) ) ;
result . chainsAdded = 1 ;
result . citiesCovered = chain . filter ( ( city ) => pathTouchesCell ( path , city . x , city . y , 1.2 ) ) . length ;
}
return result ;
}
2026-08-11 21:51:07 +09:00
const railDebug = cityRailChain ( 75000 ) ;
2026-05-28 02:47:33 +09:00
debug . railCityChainsAdded = railDebug . chainsAdded ;
debug . railCityChainCitiesCovered = railDebug . citiesCovered ;
2026-08-11 21:51:07 +09:00
const expressDebug = ensureMajorCityExpresswayLinks ( 60000 ) ;
2026-05-28 02:47:33 +09:00
debug . expresswayMajorCityLinksAdded = expressDebug . added ;
2026-05-28 15:48:42 +09:00
debug . expresswayMajorCityLinksCovered = expressDebug . covered ;
debug . expresswayMajorCityLinksNoTarget = expressDebug . noTarget ;
debug . expresswayMajorCityLinksNoPath = expressDebug . noPath ;
2026-05-28 02:47:33 +09:00
2026-08-11 21:51:07 +09:00
// Expressway finalization after administration. Smooth only when the
// terrain-safe smoother actually improves curvature, and keep the same
// narrow-strait contract used by mandatory service routing. The former
// 20-cell sea allowance could turn a repair into an implausibly straight
// long bridge.
2026-05-28 00:30:09 +09:00
for ( let i = 0 ; i < expressways . length ; i ++ ) {
2026-08-11 21:51:07 +09:00
const before = expressways [ i ] ;
const smoothed = terrainSafeSmooth ( before , terrain , "expressway" , 4 ) ;
2026-05-28 00:30:09 +09:00
if ( smoothed . length >= 2 ) {
2026-05-28 15:48:42 +09:00
const runs = pathTerrainRuns ( smoothed , terrain ) ;
2026-08-11 21:51:07 +09:00
const turns = pathSharpTurnStats ( smoothed ) ;
if ( runs . maxTunnelRun <= 24 && runs . maxSeaRun <= 3 && turns . consecutiveExtreme <= 1 ) {
2026-05-28 15:48:42 +09:00
expressways [ i ] = smoothed ;
2026-08-11 21:51:07 +09:00
if ( smoothed !== before ) debug . expresswaysSmoothed ++ ;
2026-05-28 15:48:42 +09:00
}
2026-05-28 00:30:09 +09:00
}
}
2026-05-28 15:48:42 +09:00
const expresswayBeforeTerrainPrune = expressways . length ;
for ( let i = expressways . length - 1 ; i >= 0 ; i -- ) {
const runs = pathTerrainRuns ( expressways [ i ] , terrain ) ;
2026-08-11 21:51:07 +09:00
const turns = pathSharpTurnStats ( expressways [ i ] ) ;
if ( runs . maxTunnelRun > 24 || runs . maxSeaRun > 3 || turns . consecutiveExtreme > 1 || turns . sharpShare > 0.42 ) expressways . splice ( i , 1 ) ;
2026-05-28 15:48:42 +09:00
}
debug . expresswaysPrunedForBridgeTunnelLimits = expresswayBeforeTerrainPrune - expressways . length ;
function pointInsideCityNodeBuffer ( x , y ) {
for ( const city of features . modernCities || [ ] ) {
if ( ! city || ( city . population || 0 ) < 25000 ) continue ;
const r = Math . max ( 4.2 , ( city . coreRadius || 3 ) + 1.6 ) ;
if ( Math . hypot ( x - city . x , y - city . y ) <= r ) return true ;
}
return false ;
}
function splitExpresswayAwayFromCityNodes ( path ) {
const chunks = [ ] ;
let cur = [ ] ;
for ( const [ x , y ] of path || [ ] ) {
if ( pointInsideCityNodeBuffer ( x , y ) ) {
if ( cur . length >= 2 ) chunks . push ( cur ) ;
cur = [ ] ;
continue ;
}
if ( ! cur . length || cur [ cur . length - 1 ] [ 0 ] !== x || cur [ cur . length - 1 ] [ 1 ] !== y ) cur . push ( [ x , y ] ) ;
}
if ( cur . length >= 2 ) chunks . push ( cur ) ;
return chunks . filter ( ( chunk ) => pathLengthCells ( chunk ) >= 12 ) ;
}
const expresswayBeforeCityNodePrune = expressways . length ;
const separatedExpressways = [ ] ;
for ( const path of expressways ) separatedExpressways . push ( ... splitExpresswayAwayFromCityNodes ( path ) ) ;
expressways . length = 0 ;
expressways . push ( ... dedupePaths ( separatedExpressways , 2 ) ) ;
debug . expresswaysPrunedForCityNodeSeparation = expresswayBeforeCityNodePrune - expressways . length ;
function connectNearbyExpresswayTermini ( ) {
const result = { candidates : 0 , added : 0 , failed : 0 } ;
const expressGroups = [
{ key : "expressway" , paths : expressways } ,
{ key : "externalExpressway" , paths : externalExpressways } ,
] ;
const endpoints = [ ] ;
for ( const group of expressGroups ) {
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 ) || terrain ? . sea ? . [ indexOf ( x , y ) ] || pointInsideCityNodeBuffer ( x , y ) ) continue ;
endpoints . push ( { group : group . key , pathIdx , end , x , y } ) ;
}
}
}
const pairs = [ ] ;
for ( let i = 0 ; i < endpoints . length ; i ++ ) {
const a = endpoints [ i ] ;
for ( let j = i + 1 ; j < endpoints . length ; j ++ ) {
const b = endpoints [ j ] ;
if ( a . group === b . group && a . pathIdx === b . pathIdx ) continue ;
const d = Math . hypot ( a . x - b . x , a . y - b . y ) ;
if ( d < 3.0 || d > 24.0 ) continue ;
pairs . push ( { a , b , d , kind : "terminus-terminus" } ) ;
}
}
// Also snap a dead-end to the side of a nearby expressway if no terminal is
// close enough. This removes visible half-built expressway stubs without
// requiring every segment to be merged into a single polyline.
for ( const a of endpoints ) {
let best = null ;
for ( const group of expressGroups ) {
for ( let pathIdx = 0 ; pathIdx < ( group . paths ? . length || 0 ) ; pathIdx ++ ) {
if ( a . group === group . key && a . pathIdx === pathIdx ) continue ;
const path = group . paths [ pathIdx ] ;
for ( let k = 1 ; k < ( path ? . length || 0 ) - 1 ; k += 2 ) {
const [ x , y ] = path [ k ] ;
const d = Math . hypot ( a . x - x , a . y - y ) ;
if ( d < 3.0 || d > 14.0 ) continue ;
if ( ! best || d < best . d ) best = { a , b : { group : group . key , pathIdx , end : - 1 , x , y } , d , kind : "terminus-side" } ;
}
}
}
if ( best ) pairs . push ( best ) ;
}
pairs . sort ( ( a , b ) => a . d - b . d || ( a . kind === "terminus-terminus" ? - 1 : 1 ) ) ;
const used = new Set ( ) ;
for ( const pair of pairs ) {
2026-08-11 21:51:07 +09:00
if ( result . added >= 2 ) break ;
2026-05-28 15:48:42 +09:00
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 ) || ( pair . b . end >= 0 && used . has ( bk ) ) ) continue ;
result . candidates ++ ;
2026-08-11 21:51:07 +09:00
let path = terrainFirstConnector ( pair . a , pair . b , terrain , { maxLength : pair . d * 2.7 + 42 , maxSeaRun : 0 , maxTunnelRun : 10 , shortFallback : 7 } ) ;
2026-05-28 15:48:42 +09:00
if ( ! path . length ) path = routeTerrainPath ( pair . a , pair . b , terrain , { maxLength : pair . d * 2.6 + 44 , maxSeaRun : 0 , maxTunnelRun : 10 , snapRadius : 1.8 } ) ;
2026-08-11 21:51:07 +09:00
if ( ! path . length || pathLengthCells ( path ) < 3 || ! trunkElevationSafe ( path , terrain , 0.72 ) ) { result . failed ++ ; continue ; }
2026-05-28 15:48:42 +09:00
const runs = pathTerrainRuns ( path , terrain ) ;
2026-08-11 21:51:07 +09:00
if ( runs . maxTunnelRun > 18 || runs . maxSeaRun > 3 || pathSharpTurnStats ( path ) . consecutiveExtreme > 1 ) { result . failed ++ ; continue ; }
expressways . push ( terrainSafeSmooth ( path , terrain , "expressway" , 3 ) ) ;
2026-05-28 15:48:42 +09:00
used . add ( ak ) ;
if ( pair . b . end >= 0 ) used . add ( bk ) ;
result . added ++ ;
}
return result ;
}
const expresswayTerminusConnectDebug = connectNearbyExpresswayTermini ( ) ;
debug . expresswayTerminusConnectionsAdded = expresswayTerminusConnectDebug . added ;
debug . expresswayTerminusConnectionCandidates = expresswayTerminusConnectDebug . candidates ;
debug . expresswayTerminusConnectionFailures = expresswayTerminusConnectDebug . failed ;
function pointOnExpressway ( p , radius = 1.5 ) {
return ( expressways || [ ] ) . some ( ( path ) => pathTouchesCell ( path , p . x , p . y , radius ) ) ;
}
const icBeforePrune = interchanges . length ;
const pairedInterchanges = [ ] ;
const pairedAccessRoads = [ ] ;
for ( let i = 0 ; i < interchanges . length ; i ++ ) {
const ic = interchanges [ i ] ;
const access = ( features . icAccessRoads || [ ] ) [ i ] ;
if ( ic && pointOnExpressway ( ic , 1.8 ) && access && access . length >= 2 ) {
pairedInterchanges . push ( ic ) ;
pairedAccessRoads . push ( access ) ;
}
}
interchanges . length = 0 ;
interchanges . push ( ... pairedInterchanges ) ;
features . icAccessRoads = pairedAccessRoads ;
debug . interchangesPrunedWithoutExpresswayOrAccess = icBeforePrune - interchanges . length ;
function ensureTerminalInterchangesWithAccess ( ) {
const result = { endpointsChecked : 0 , added : 0 , accessAdded : 0 , withoutAccess : 0 } ;
features . icAccessRoads || = [ ] ;
const ordinaryRoads = [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) , ... ( features . minorRoads || [ ] ) ] ;
for ( const path of [ ... ( features . expressways || [ ] ) , ... ( features . externalExpressways || [ ] ) ] ) {
if ( ! path || path . length < 2 ) continue ;
for ( const raw of [ path [ 0 ] , path [ path . length - 1 ] ] ) {
const p = { x : Math . round ( raw [ 0 ] ) , y : Math . round ( raw [ 1 ] ) } ;
result . endpointsChecked ++ ;
if ( ! inside ( p . x , p . y ) || terrain ? . sea ? . [ indexOf ( p . x , p . y ) ] ) continue ;
if ( ( interchanges || [ ] ) . some ( ( ic ) => Math . hypot ( ic . x - p . x , ic . y - p . y ) <= 2.8 ) ) continue ;
const hit = nearestPointOnPaths ( ordinaryRoads , p , 58 ) ;
let access = [ ] ;
if ( hit ) {
const d = Math . hypot ( p . x - hit . x , p . y - hit . y ) ;
2026-08-11 21:51:07 +09:00
access = terrainFirstConnector ( p , hit , terrain , { maxLength : d * 2.35 + 22 , maxSeaRun : 0 , maxTunnelRun : 8 , maxExpanded : Math . min ( SIZE , Math . max ( 7000 , Math . floor ( d * d * 8 + 4000 ) ) ) , maxElevation : 0.86 } ) ;
2026-05-28 15:48:42 +09:00
if ( access . length >= 2 ) {
features . icAccessRoads . push ( access ) ;
features . minorRoads || = [ ] ;
features . minorRoads . push ( access ) ;
result . accessAdded ++ ;
}
}
addInterchange ( interchanges , p . x , p . y , access . length >= 2 ? "post-admin-terminal-ic" : "post-admin-terminal-ic-no-access" ) ;
result . added ++ ;
if ( access . length < 2 ) result . withoutAccess ++ ;
}
}
return result ;
2026-05-28 00:30:09 +09:00
}
2026-05-28 15:48:42 +09:00
const terminalIcDebug = ensureTerminalInterchangesWithAccess ( ) ;
debug . expresswayTerminalInterchangesAdded = terminalIcDebug . added ;
debug . expresswayTerminalInterchangeAccessAdded = terminalIcDebug . accessAdded ;
debug . expresswayTerminalInterchangesWithoutAccess = terminalIcDebug . withoutAccess ;
2026-05-28 00:30:09 +09:00
2026-08-11 21:51:07 +09:00
function connectNearbyPathTermini ( pathGroups , mode , options = { } ) {
const result = { mode , candidates : 0 , added : 0 , failed : 0 } ;
const endpoints = [ ] ;
pathGroups . forEach ( ( group ) => {
for ( let pathIdx = 0 ; pathIdx < ( group . paths ? . length || 0 ) ; pathIdx ++ ) {
const path = group . paths [ pathIdx ] ;
if ( ! path || path . length < 2 ) continue ;
const ends = [ path [ 0 ] , path [ path . length - 1 ] ] ;
ends . forEach ( ( raw , end ) => {
const x = Math . round ( raw [ 0 ] ) , y = Math . round ( raw [ 1 ] ) ;
if ( ! inside ( x , y ) || terrain ? . sea ? . [ indexOf ( x , y ) ] ) return ;
endpoints . push ( { group : group . key , pathIdx , end , x , y } ) ;
} ) ;
}
} ) ;
const pairs = [ ] ;
for ( let i = 0 ; i < endpoints . length ; i ++ ) {
const a = endpoints [ i ] ;
for ( let j = i + 1 ; j < endpoints . length ; j ++ ) {
const b = endpoints [ j ] ;
if ( a . group === b . group && a . pathIdx === b . pathIdx ) continue ;
const d = Math . hypot ( a . x - b . x , a . y - b . y ) ;
if ( d < ( options . minDistance ? ? 2.5 ) || d > ( options . maxDistance ? ? 16 ) ) continue ;
pairs . push ( { a , b , d } ) ;
}
}
pairs . sort ( ( a , b ) => a . d - b . d ) ;
const used = new Set ( ) ;
for ( const pair of pairs ) {
if ( result . added >= ( options . maxAdded ? ? 10 ) ) 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 ;
result . candidates ++ ;
let path = routeTerrainPath ( pair . a , pair . b , terrain , {
maxLength : pair . d * ( options . routeDetour ? ? 2.4 ) + ( options . routeSlack ? ? 22 ) ,
maxSeaRun : options . maxSeaRun ? ? 0 ,
maxTunnelRun : options . maxTunnelRun ? ? ( mode === 'rail' ? 12 : 8 ) ,
snapRadius : options . snapRadius ? ? 1.6 ,
} ) ;
if ( ! path . length ) path = terrainFirstConnector ( pair . a , pair . b , terrain , {
skipPrimaryRoute : true ,
maxLength : pair . d * ( options . routeDetour ? ? 2.8 ) + ( options . routeSlack ? ? 28 ) ,
maxSeaRun : options . maxSeaRun ? ? 0 ,
maxTunnelRun : options . maxTunnelRun ? ? ( mode === 'rail' ? 12 : 8 ) ,
maxExpanded : Math . min ( SIZE , Math . max ( 9000 , Math . floor ( pair . d * pair . d * 7 + 6000 ) ) ) ,
maxElevation : mode === 'local' ? 0.86 : 0.72 ,
} ) ;
if ( ! path . length || pathLengthCells ( path ) < 3 || ( mode !== 'local' && ! trunkElevationSafe ( path , terrain , 0.72 ) ) ) { result . failed ++ ; continue ; }
pathGroups [ 0 ] . paths . push ( smoothPath ( path , 1 ) ) ;
used . add ( ak ) ; used . add ( bk ) ;
result . added ++ ;
}
return result ;
}
// Urban local-road densification is handled only by mapTransport.js's existing terrain-aware local-access algorithm.
function ensureMajorCityNationalRoadLinks ( minPopulation = 50000 ) {
const cities = ( features . modernCities || [ ] )
. filter ( ( city ) => city && inside ( city . x , city . y ) && ! terrain ? . sea ? . [ indexOf ( city . x , city . y ) ] && ( ( city . population || 0 ) >= minPopulation || city . isPrefecturalCapital || city . isRegionalCapital ) )
. sort ( ( a , b ) => ( b . population || 0 ) - ( a . population || 0 ) ) ;
const result = { minPopulation , checked : cities . length , covered : 0 , added : 0 , noTarget : 0 , noPath : 0 } ;
features . nationalRoads || = [ ] ;
for ( const city of cities ) {
const network = [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] ;
if ( anyPathTouches ( network , city , 3.0 ) ) { result . covered ++ ; continue ; }
const cityComponent = componentAt ( city ) ;
let targets = nearestPointsOnPaths ( network , city , Infinity , 28 , 3 ) . filter ( ( target ) => componentAt ( target ) === cityComponent ) . slice ( 0 , 14 ) ;
if ( ! targets . length ) {
targets = cities . filter ( ( q ) => q !== city )
. map ( ( q ) => ( { ... q , d : Math . hypot ( q . x - city . x , q . y - city . y ) } ) )
. filter ( ( q ) => q . d >= 10 && q . d <= 180 && sameLandComponent ( city , q ) )
. sort ( ( a , b ) => a . d - b . d ) . slice ( 0 , 8 ) ;
}
if ( ! targets . length ) {
targets = sameComponentCivicTargets ( city , { minDistance : 8 , maxDistance : 220 , limit : 16 } ) ;
}
if ( ! targets . length ) { result . noTarget ++ ; continue ; }
let path = [ ] ;
for ( const target of targets ) {
const d = Math . hypot ( target . x - city . x , target . y - city . y ) ;
path = routeTerrainPath ( city , target , terrain , { maxLength : d * 3.0 + 72 , maxSeaRun : 0 , maxTunnelRun : 20 , snapRadius : 2.0 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! path . length ) path = terrainFirstConnector ( city , target , terrain , { skipPrimaryRoute : true , maxLength : d * 3.05 + 64 , maxSeaRun : 0 , maxTunnelRun : 20 , shortFallback : 7 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! path . length ) path = routeLandConnectedTerrainFallback ( city , target , terrain , { maxLength : SIZE , maxElevation : 0.695 } ) ;
if ( path . length >= ( d < 4 ? 2 : 4 ) && trunkElevationSafe ( path , terrain , 0.72 ) ) break ;
path = [ ] ;
}
if ( ! path . length || pathLengthCells ( path ) < 2 ) { result . noPath ++ ; continue ; }
path = trimRouteAtExistingNetwork ( path , network , 2.6 , 3 ) ;
path = terrainSafeSmooth ( path , terrain , "national" , 2 ) ;
if ( ! hardTerrainPathValid ( path , "national" ) ) { result . noPath ++ ; continue ; }
features . nationalRoads . push ( path ) ;
result . added ++ ;
}
return result ;
}
function ensureRuralNationalRoadCoverage ( ) {
features . nationalRoads || = [ ] ;
const result = {
checked : 0 , eligible : 0 , alreadyServed : 0 , added : 0 , noTarget : 0 , noPath : 0 ,
forcedRemote : 0 , alternateTargetsTried : 0 ,
} ;
const candidates = ( adminCenters || [ ] )
. filter ( ( p ) => p && inside ( p . x , p . y ) && ! terrain ? . sea ? . [ indexOf ( p . x , p . y ) ] )
. map ( ( p ) => ( { ... p , population : Number ( p . municipalityPopulation || p . population || 0 ) } ) )
. filter ( ( p ) => p . population >= 2500 && p . population < 42000 ) ;
const areaScale = Math . max ( 1 , SIZE / ( 258 * 183 ) ) ;
// The old fixed cap (roughly a dozen roads on the overscan map) left whole
// rural districts without a national-road corridor. Scale the budget with
// the actual number of rural municipalities, while keeping it bounded so
// difficult mountain maps cannot turn this guarantee into an unbounded A* loop.
const maxAdded = Math . min ( candidates . length , Math . max ( 18 , Math . round ( 7 + candidates . length * 0.46 + 2 * Math . sqrt ( areaScale ) ) ) ) ;
const profileFor = ( pop ) => pop >= 18000
? { serviceRadius : 8.0 , probability : 1.00 , remoteFloor : 22 }
: pop >= 10000 ? { serviceRadius : 9.0 , probability : 0.96 , remoteFloor : 25 }
: pop >= 6000 ? { serviceRadius : 10.0 , probability : 0.84 , remoteFloor : 29 }
: pop >= 3500 ? { serviceRadius : 11.0 , probability : 0.68 , remoteFloor : 34 }
: { serviceRadius : 12.0 , probability : 0.50 , remoteFloor : 40 } ;
// At this late stage the local-road network is already terrain-routed and
// connected. Reusing a continuous local-road corridor as a national-road
// alignment is both cheaper and more realistic than solving a second,
// parallel A* route beside it. Only corridors that also satisfy national
// terrain constraints are promoted.
const roadMask = new Uint8Array ( SIZE ) ;
const nationalMask = new Uint8Array ( SIZE ) ;
const rasterizeToMask = ( path , mask ) => {
for ( let k = 1 ; k < ( path ? . length || 0 ) ; k ++ ) {
const a = path [ k - 1 ] , b = path [ k ] ;
const steps = Math . max ( 1 , Math . ceil ( Math . hypot ( b [ 0 ] - a [ 0 ] , b [ 1 ] - a [ 1 ] ) ) ) ;
for ( let q = 0 ; q <= steps ; q ++ ) {
const t = q / steps ;
const x = Math . round ( a [ 0 ] + ( b [ 0 ] - a [ 0 ] ) * t ) , y = Math . round ( a [ 1 ] + ( b [ 1 ] - a [ 1 ] ) * t ) ;
if ( inside ( x , y ) ) mask [ indexOf ( x , y ) ] = 1 ;
}
}
} ;
for ( const path of [ ... ( features . minorRoads || [ ] ) , ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] ) rasterizeToMask ( path , roadMask ) ;
for ( const path of [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] ) rasterizeToMask ( path , nationalMask ) ;
const roadSeen = new Uint32Array ( SIZE ) ;
const roadPrev = new Int32Array ( SIZE ) ;
const roadQueue = new Int32Array ( SIZE ) ;
let roadStamp = 0 ;
const roadDirs = [ [ 1 , 0 ] , [ - 1 , 0 ] , [ 0 , 1 ] , [ 0 , - 1 ] , [ 1 , 1 ] , [ 1 , - 1 ] , [ - 1 , 1 ] , [ - 1 , - 1 ] ] ;
function promoteExistingRoadCorridor ( point , maxCells = 180 ) {
let start = - 1 , startD2 = Infinity ;
const sx0 = Math . max ( 0 , Math . floor ( point . x - 6 ) ) , sx1 = Math . min ( MAP _W - 1 , Math . ceil ( point . x + 6 ) ) ;
const sy0 = Math . max ( 0 , Math . floor ( point . y - 6 ) ) , sy1 = Math . min ( MAP _H - 1 , Math . ceil ( point . y + 6 ) ) ;
for ( let y = sy0 ; y <= sy1 ; y ++ ) for ( let x = sx0 ; x <= sx1 ; x ++ ) {
const i = indexOf ( x , y ) ; if ( ! roadMask [ i ] ) continue ;
const d2 = ( x - point . x ) * * 2 + ( y - point . y ) * * 2 ;
if ( d2 < startD2 ) { startD2 = d2 ; start = i ; }
}
if ( start < 0 || startD2 > 36 ) return [ ] ;
roadStamp = ( roadStamp + 1 ) >>> 0 ;
if ( ! roadStamp ) { roadSeen . fill ( 0 ) ; roadStamp = 1 ; }
let head = 0 , tail = 0 , found = - 1 ;
roadQueue [ tail ++ ] = start ; roadSeen [ start ] = roadStamp ; roadPrev [ start ] = - 1 ;
while ( head < tail && head < maxCells * 28 ) {
const cur = roadQueue [ head ++ ] ;
if ( cur !== start && nationalMask [ cur ] ) { found = cur ; break ; }
const x = cur % MAP _W , y = ( cur / MAP _W ) | 0 ;
for ( const [ dx , dy ] of roadDirs ) {
const nx = x + dx , ny = y + dy ;
if ( nx < 0 || ny < 0 || nx >= MAP _W || ny >= MAP _H ) continue ;
const ni = ny * MAP _W + nx ;
if ( ! roadMask [ ni ] || roadSeen [ ni ] === roadStamp ) continue ;
roadSeen [ ni ] = roadStamp ; roadPrev [ ni ] = cur ; roadQueue [ tail ++ ] = ni ;
}
}
if ( found < 0 ) return [ ] ;
const rev = [ ] ;
for ( let cur = found ; cur >= 0 ; cur = roadPrev [ cur ] ) {
rev . push ( [ cur % MAP _W , ( cur / MAP _W ) | 0 ] ) ;
if ( cur === start || rev . length > maxCells ) break ;
}
if ( ! rev . length || rev [ rev . length - 1 ] [ 0 ] !== start % MAP _W || rev [ rev . length - 1 ] [ 1 ] !== ( ( start / MAP _W ) | 0 ) ) return [ ] ;
const path = rev . reverse ( ) ;
if ( path . length < 4 || path . length > maxCells || ! hardTerrainPathValid ( path , "national" ) ) return [ ] ;
return path ;
}
// Rank genuinely underserved municipalities first. This avoids spending the
// route budget on already-near-trunk towns while leaving a 50-100 cell rural
// void untouched.
const initialNetwork = [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] ;
const ranked = candidates . map ( ( point ) => {
const profile = profileFor ( point . population || 0 ) ;
const hit = nearestPointOnPaths ( initialNetwork , point , 150 ) ;
const distance = hit ? . d ? ? 180 ;
const deficit = distance / Math . max ( 1 , profile . serviceRadius ) ;
return { point , profile , initialDistance : distance , score : deficit * 10 + Math . log1p ( point . population || 0 ) } ;
} ) . sort ( ( a , b ) => b . score - a . score || b . point . population - a . point . population || a . point . y - b . point . y || a . point . x - b . point . x ) ;
for ( const row of ranked ) {
if ( result . added >= maxAdded ) break ;
const point = row . point ;
const pop = point . population || 0 ;
const profile = row . profile ;
result . checked ++ ;
let network = [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] ;
const currentHit = nearestPointOnPaths ( network , point , 150 ) ;
if ( currentHit && currentHit . d <= profile . serviceRadius ) { result . alreadyServed ++ ; continue ; }
const remote = ! currentHit || currentHit . d >= profile . remoteFloor ;
const drawKey = Math . round ( point . x ) * 131 + Math . round ( point . y ) * 197 + Math . floor ( pop / 500 ) ;
if ( ! remote && rand ( seed + 84217 , drawKey ) > profile . probability ) continue ;
if ( remote ) result . forcedRemote ++ ;
result . eligible ++ ;
const promoted = promoteExistingRoadCorridor ( point , remote ? 190 : 140 ) ;
if ( promoted . length ) {
features . nationalRoads . push ( promoted ) ;
rasterizeToMask ( promoted , nationalMask ) ;
rasterizeToMask ( promoted , roadMask ) ;
result . promotedLocalCorridors = ( result . promotedLocalCorridors || 0 ) + 1 ;
result . added ++ ;
continue ;
}
// Try several physically distinct attachment points. The previous single-
// target policy was the main source of false failures: one ridge-blocked
// nearest point caused the municipality to be abandoned even when another
// nearby valley corridor existed on the same trunk network.
const targetLimit = pop >= 10000 || remote ? 4 : 3 ;
const maxTargetDistance = remote ? 145 : ( pop >= 12000 ? 105 : 88 ) ;
const targets = nearestPointsOnPaths ( network , point , maxTargetDistance , targetLimit , 5 )
. filter ( ( target ) => sameLandComponent ( point , target ) ) ;
if ( currentHit && currentHit . d <= maxTargetDistance && sameLandComponent ( point , currentHit )
&& ! targets . some ( ( q ) => Math . hypot ( q . x - currentHit . x , q . y - currentHit . y ) < 4 ) ) targets . unshift ( currentHit ) ;
if ( remote ) {
// A remote municipality may sit behind a ridge from the geometrically
// nearest trunk. Also try nearby municipalities in the same land
// component so a valley-following regional trunk can grow toward the
// existing network over several municipalities.
const peers = candidates
. filter ( ( q ) => q !== point && sameLandComponent ( point , q ) )
. map ( ( q ) => ( { ... q , d : Math . hypot ( q . x - point . x , q . y - point . y ) } ) )
. filter ( ( q ) => q . d >= 7 && q . d <= 72 )
. sort ( ( a , b ) => a . d - b . d || b . population - a . population )
. slice ( 0 , 2 ) ;
for ( const peer of peers ) if ( ! targets . some ( ( q ) => Math . hypot ( q . x - peer . x , q . y - peer . y ) < 4 ) ) targets . push ( peer ) ;
}
if ( ! targets . length ) {
const civic = [ ... ( features . modernCities || [ ] ) , ... ( features . markets || [ ] ) , ... ( adminCenters || [ ] ) ]
. filter ( ( q ) => q && q !== point && inside ( q . x , q . y ) && sameLandComponent ( point , q ) )
. map ( ( q ) => ( { ... q , d : Math . hypot ( q . x - point . x , q . y - point . y ) , p : Number ( q . municipalityPopulation || q . population || 0 ) } ) )
. filter ( ( q ) => q . d >= 9 && q . d <= 105 && q . p >= Math . max ( 6000 , pop * 1.05 ) )
. sort ( ( a , b ) => ( a . d - b . d ) || ( b . p - a . p ) ) . slice ( 0 , 3 ) ;
targets . push ( ... civic ) ;
}
if ( ! targets . length ) {
// Some islands / mountain basins have no pre-existing national-road
// object at all. Seed a trunk within that land component instead of
// declaring every municipality there permanently unserviceable.
const componentPeers = candidates
. filter ( ( q ) => q !== point && sameLandComponent ( point , q ) )
. map ( ( q ) => ( { ... q , d : Math . hypot ( q . x - point . x , q . y - point . y ) } ) )
. filter ( ( q ) => q . d >= 8 && q . d <= 115 )
. sort ( ( a , b ) => ( b . population - a . population ) || ( a . d - b . d ) ) ;
const stronger = componentPeers . filter ( ( q ) => q . population >= Math . max ( 3500 , pop * 0.9 ) ) . slice ( 0 , 2 ) ;
targets . push ( ... ( stronger . length ? stronger : componentPeers . slice ( 0 , 2 ) ) ) ;
}
if ( ! targets . length ) { result . noTarget ++ ; continue ; }
// Bound alternative terrain searches so mountainous seeds cannot explode
// in runtime. Multiple candidates remain enough to fix the old
// single-target false-negative behavior.
if ( targets . length > 4 ) {
targets . sort ( ( a , b ) => {
const ad = Math . hypot ( a . x - point . x , a . y - point . y ) ;
const bd = Math . hypot ( b . x - point . x , b . y - point . y ) ;
return ad - bd ;
} ) ;
targets . length = 4 ;
}
let path = [ ] ;
for ( let ti = 0 ; ti < targets . length ; ti ++ ) {
const target = targets [ ti ] ;
if ( ti > 0 ) result . alternateTargetsTried ++ ;
const d = Math . hypot ( target . x - point . x , target . y - point . y ) ;
let candidate = routeTerrainPath ( point , target , terrain , {
maxLength : d * 3.0 + 72 , maxSeaRun : 0 , maxTunnelRun : 18 ,
snapRadius : 1.8 , maxExpanded : SIZE , maxElevation : 0.695 ,
} ) ;
if ( ! candidate . length ) candidate = terrainFirstConnector ( point , target , terrain , {
skipPrimaryRoute : true , maxLength : d * 3.2 + 82 , maxSeaRun : 0 ,
maxTunnelRun : 18 , shortFallback : 7 , maxExpanded : SIZE , maxElevation : 0.695 ,
} ) ;
if ( ! candidate . length ) candidate = routeLandConnectedTerrainFallback ( point , target , terrain , {
maxLength : Math . min ( SIZE , d * 4.1 + 104 ) , maxElevation : 0.695 ,
} ) ;
if ( candidate . length < 4 || ! trunkElevationSafe ( candidate , terrain , 0.72 ) ) continue ;
network = [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] ;
candidate = trimRouteAtExistingNetwork ( candidate , network , 2.8 , 3 ) ;
candidate = terrainSafeSmooth ( candidate , terrain , "national" , 2 ) ;
if ( candidate . length < 3 || ! hardTerrainPathValid ( candidate , "national" ) ) continue ;
path = candidate ;
break ;
}
if ( ! path . length ) { result . noPath ++ ; continue ; }
features . nationalRoads . push ( path ) ;
rasterizeToMask ( path , nationalMask ) ;
rasterizeToMask ( path , roadMask ) ;
result . added ++ ;
}
// If strict new-trunk routing still cannot cross a local ridge, reuse an
// already-built terrain-valid local-road alignment through the municipal
// seat. This increases rural national-road presence without inventing a
// simplified straight road or a side-by-side duplicate corridor.
const promotionBudget = Math . min ( 10 , Math . max ( 4 , Math . round ( candidates . length * 0.16 ) ) ) ;
let promotedAfterRouting = 0 ;
for ( const point of candidates ) {
if ( promotedAfterRouting >= promotionBudget || result . added >= maxAdded ) break ;
const profile = profileFor ( point . population || 0 ) ;
const network = [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] ;
if ( anyPathTouches ( network , point , profile . serviceRadius ) ) continue ;
let best = null ;
for ( const path of features . minorRoads || [ ] ) {
if ( ! path || path . length < 8 || ! pathTouchesCell ( path , point . x , point . y , 3.2 ) ) continue ;
if ( ! hardTerrainPathValid ( path , "national" ) ) continue ;
const a = path [ 0 ] , b = path [ path . length - 1 ] ;
const da = nearestPointOnPaths ( network , { x : a [ 0 ] , y : a [ 1 ] } , 80 ) ? . d ? ? 80 ;
const db = nearestPointOnPaths ( network , { x : b [ 0 ] , y : b [ 1 ] } , 80 ) ? . d ? ? 80 ;
const score = Math . min ( da , db ) - Math . min ( 24 , pathLengthCells ( path ) ) * 0.08 ;
if ( ! best || score < best . score ) best = { path , score } ;
}
if ( ! best ) continue ;
const promotedPath = best . path . map ( ( q ) => [ q [ 0 ] , q [ 1 ] ] ) ;
features . nationalRoads . push ( promotedPath ) ;
rasterizeToMask ( promotedPath , nationalMask ) ;
rasterizeToMask ( promotedPath , roadMask ) ;
promotedAfterRouting ++ ;
result . added ++ ;
}
result . promotedRuralLocalAlignments = promotedAfterRouting ;
result . maxAdded = maxAdded ;
return result ;
}
function repairUrbanNationalRoadGaps ( ) {
features . nationalRoads || = [ ] ;
const result = {
citiesChecked : 0 , candidateGaps : 0 , attemptedPairs : 0 , alternatePairsTried : 0 ,
added : 0 , noPath : 0 , unresolvedCities : 0 ,
} ;
const cities = ( features . modernCities || [ ] )
. filter ( ( city ) => city && inside ( city . x , city . y ) && ! terrain ? . sea ? . [ indexOf ( city . x , city . y ) ] && ( city . population || 0 ) >= 45000 )
. sort ( ( a , b ) => ( b . population || 0 ) - ( a . population || 0 ) ) ;
const maxAdded = Math . max ( 12 , Math . round ( cities . length * 1.25 ) ) ;
for ( const city of cities ) {
if ( result . added >= maxAdded ) break ;
result . citiesChecked ++ ;
const radius = Math . max ( 13 , Math . min ( 30 , ( city . urbanRadius || 10 ) * 2.0 ) ) ;
const rows = [ ] ;
for ( let pi = 0 ; pi < features . nationalRoads . length ; pi ++ ) {
const path = features . nationalRoads [ pi ] ;
if ( ! path ? . length ) continue ;
for ( const tuple of [ path [ 0 ] , path [ path . length - 1 ] ] ) {
const d = Math . hypot ( tuple [ 0 ] - city . x , tuple [ 1 ] - city . y ) ;
if ( d <= radius ) rows . push ( { x : tuple [ 0 ] , y : tuple [ 1 ] , pathIndex : pi , d } ) ;
}
}
const pairs = [ ] ;
for ( let a = 0 ; a < rows . length ; a ++ ) for ( let b = a + 1 ; b < rows . length ; b ++ ) {
if ( rows [ a ] . pathIndex === rows [ b ] . pathIndex ) continue ;
const gap = Math . hypot ( rows [ a ] . x - rows [ b ] . x , rows [ a ] . y - rows [ b ] . y ) ;
if ( gap <= 1.0 || gap > 20 || ! sameLandComponent ( rows [ a ] , rows [ b ] ) ) continue ;
pairs . push ( { a : rows [ a ] , b : rows [ b ] , gap , score : gap + 0.12 * ( rows [ a ] . d + rows [ b ] . d ) } ) ;
}
pairs . sort ( ( u , v ) => u . score - v . score || u . gap - v . gap ) ;
if ( ! pairs . length ) continue ;
result . candidateGaps += pairs . length ;
let connector = [ ] ;
// One successful connector per city is enough here. If the shortest gap
// is terrain-blocked, try several other endpoint pairs instead of
// abandoning the city after one failed A* call.
for ( let pi = 0 ; pi < Math . min ( 6 , pairs . length ) ; pi ++ ) {
const pair = pairs [ pi ] ;
result . attemptedPairs ++ ;
if ( pi > 0 ) result . alternatePairsTried ++ ;
let candidate = routeTerrainPath ( pair . a , pair . b , terrain , {
maxLength : pair . gap * 3.0 + 26 , maxSeaRun : 0 , maxTunnelRun : 12 ,
snapRadius : 1.2 , maxExpanded : Math . min ( SIZE , 20000 ) , maxElevation : 0.72 ,
} ) ;
if ( ! candidate . length ) candidate = terrainFirstConnector ( pair . a , pair . b , terrain , {
skipPrimaryRoute : true , maxLength : pair . gap * 3.35 + 30 , maxSeaRun : 0 ,
maxTunnelRun : 12 , maxExpanded : Math . min ( SIZE , 24000 ) , maxElevation : 0.72 ,
} ) ;
if ( ! candidate . length ) candidate = routeLandConnectedTerrainFallback ( pair . a , pair . b , terrain , {
maxLength : Math . min ( SIZE , pair . gap * 4.0 + 38 ) , maxElevation : 0.695 ,
} ) ;
if ( candidate . length < 3 ) continue ;
candidate = terrainSafeSmooth ( candidate , terrain , "national" , 1 ) ;
if ( ! hardTerrainPathValid ( candidate , "national" ) ) continue ;
connector = candidate ;
break ;
}
if ( ! connector . length ) { result . noPath ++ ; result . unresolvedCities ++ ; continue ; }
features . nationalRoads . push ( connector ) ;
result . added ++ ;
}
result . maxAdded = maxAdded ;
return result ;
}
function ensureMajorCityRailLinks ( minPopulation = 50000 ) {
const cities = ( features . modernCities || [ ] )
. filter ( ( city ) => city && inside ( city . x , city . y ) && ! terrain ? . sea ? . [ indexOf ( city . x , city . y ) ] && ( ( city . population || 0 ) >= minPopulation || city . isPrefecturalCapital || city . isRegionalCapital ) )
. sort ( ( a , b ) => ( b . population || 0 ) - ( a . population || 0 ) ) ;
const result = { minPopulation , checked : cities . length , covered : 0 , added : 0 , noTarget : 0 , noPath : 0 } ;
features . railways || = [ ] ;
features . branchRailways || = [ ] ;
for ( const city of cities ) {
const network = [ ... ( features . railways || [ ] ) , ... ( features . externalRailways || [ ] ) , ... ( features . branchRailways || [ ] ) ] ;
if ( anyPathTouches ( network , city , 3.0 ) ) { result . covered ++ ; continue ; }
const cityComponent = componentAt ( city ) ;
let targets = nearestPointsOnPaths ( network , city , Infinity , 28 , 3 ) . filter ( ( target ) => componentAt ( target ) === cityComponent ) . slice ( 0 , 14 ) ;
if ( ! targets . length ) {
targets = cities . filter ( ( q ) => q !== city )
. map ( ( q ) => ( { ... q , d : Math . hypot ( q . x - city . x , q . y - city . y ) } ) )
. filter ( ( q ) => q . d >= 10 && q . d <= 185 && sameLandComponent ( city , q ) )
. sort ( ( a , b ) => a . d - b . d ) . slice ( 0 , 8 ) ;
}
if ( ! targets . length ) {
targets = sameComponentCivicTargets ( city , { minDistance : 8 , maxDistance : 220 , limit : 16 } ) ;
}
if ( ! targets . length ) { result . noTarget ++ ; continue ; }
let path = [ ] ;
for ( const target of targets ) {
const d = Math . hypot ( target . x - city . x , target . y - city . y ) ;
path = routeTerrainPath ( city , target , terrain , { maxLength : d * 3.1 + 78 , maxSeaRun : 0 , maxTunnelRun : 28 , snapRadius : 2.0 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! path . length ) path = terrainFirstConnector ( city , target , terrain , { skipPrimaryRoute : true , maxLength : d * 3.15 + 70 , maxSeaRun : 0 , maxTunnelRun : 28 , shortFallback : 7 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! path . length ) path = routeLandConnectedTerrainFallback ( city , target , terrain , { maxLength : SIZE , maxElevation : 0.695 } ) ;
if ( path . length >= ( d < 4 ? 2 : 4 ) && trunkElevationSafe ( path , terrain , 0.72 ) ) break ;
path = [ ] ;
}
// A major city must not be left rail-isolated merely because the nearest existing line is
// on the opposite side of a difficult local ridge. Try several same-land-component major
// cities as alternate OD targets; this still uses the full terrain router and never emits
// a direct straight fallback.
if ( ! path . length ) {
const partnerCities = cities . filter ( ( q ) => q !== city && sameLandComponent ( city , q ) )
. sort ( ( a , b ) => Math . hypot ( a . x - city . x , a . y - city . y ) - Math . hypot ( b . x - city . x , b . y - city . y ) ) ;
for ( const partner of partnerCities . slice ( 0 , 10 ) ) {
const d = Math . hypot ( partner . x - city . x , partner . y - city . y ) ;
let candidate = routeTerrainPath ( city , partner , terrain , { maxLength : d * 3.6 + 112 , maxSeaRun : 0 , maxTunnelRun : 36 , snapRadius : 2.0 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! candidate . length ) candidate = terrainFirstConnector ( city , partner , terrain , { skipPrimaryRoute : true , maxLength : d * 3.7 + 116 , maxSeaRun : 0 , maxTunnelRun : 36 , shortFallback : 7 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! candidate . length ) candidate = routeLandConnectedTerrainFallback ( city , partner , terrain , { maxLength : SIZE , maxElevation : 0.695 } ) ;
if ( candidate . length < 4 || ! trunkElevationSafe ( candidate , terrain , 0.72 ) ) continue ;
path = candidate ;
break ;
}
}
if ( ! path . length ) {
const nationalGuides = [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ]
. map ( ( guide ) => ( { guide , hit : nearestPointOnPaths ( [ guide ] , city , 6 ) } ) )
. filter ( ( row ) => row . hit && componentAt ( row . hit ) === cityComponent )
. sort ( ( a , b ) => a . hit . d - b . hit . d ) ;
for ( const row of nationalGuides . slice ( 0 , 5 ) ) {
const guided = routeAlongTerrainGuide ( city , row . guide , "rail" , { desiredLength : 58 , stepDistance : 8 } ) ;
if ( guided . length >= 4 ) { path = guided ; break ; }
}
}
if ( ! path . length || pathLengthCells ( path ) < 2 ) { result . noPath ++ ; continue ; }
path = trimRouteAtExistingNetwork ( path , network , 2.4 , 3 ) ;
path = terrainSafeSmooth ( path , terrain , "rail" , 2 ) ;
if ( ! hardTerrainPathValid ( path , "rail" ) ) { result . noPath ++ ; continue ; }
features . branchRailways . push ( path ) ;
result . added ++ ;
}
return result ;
}
function densifyRailToNationalRatio ( targetRatio = 0.75 , focusRect = null ) {
features . railways || = [ ] ;
features . branchRailways || = [ ] ;
const normalizedFocus = focusRect && [ focusRect . x0 , focusRect . y0 , focusRect . x1 , focusRect . y1 ] . every ( Number . isFinite )
? { x0 : Math . floor ( focusRect . x0 ) , y0 : Math . floor ( focusRect . y0 ) , x1 : Math . ceil ( focusRect . x1 ) , y1 : Math . ceil ( focusRect . y1 ) }
: null ;
const pointInFocus = ( x , y , margin = 0 ) => ! normalizedFocus
|| ( x >= normalizedFocus . x0 - margin && y >= normalizedFocus . y0 - margin && x < normalizedFocus . x1 + margin && y < normalizedFocus . y1 + margin ) ;
const lengthSum = ( groups ) => ( groups || [ ] ) . reduce ( ( sum , path ) => {
if ( ! normalizedFocus ) return sum + pathLengthCells ( path || [ ] ) ;
let subtotal = 0 ;
for ( let k = 1 ; k < ( path ? . length || 0 ) ; k ++ ) {
const a = path [ k - 1 ] , b = path [ k ] ;
const mx = ( a [ 0 ] + b [ 0 ] ) * 0.5 , my = ( a [ 1 ] + b [ 1 ] ) * 0.5 ;
if ( pointInFocus ( mx , my ) ) subtotal += Math . hypot ( b [ 0 ] - a [ 0 ] , b [ 1 ] - a [ 1 ] ) ;
}
return sum + subtotal ;
} , 0 ) ;
const nationalLength = lengthSum ( [ ... ( features . nationalRoads || [ ] ) ] ) ;
// Density is evaluated on the publishable centre when literal initial
// overscan is active. The pathfinder itself still sees the entire hidden
// raster, so this does not regress to edge-clipped planning.
const currentRailLength = ( ) => lengthSum ( [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) ] ) ;
const targetLength = Math . max ( 0 , nationalLength * targetRatio ) ;
const candidates = [
... ( features . modernCities || [ ] ) . filter ( ( p ) => ( p . population || 0 ) >= 12000 ) ,
... ( features . markets || [ ] ) . filter ( ( p ) => ( p . population || 0 ) >= 5000 ) ,
] . filter ( ( p ) => p && inside ( p . x , p . y ) && ! terrain ? . sea ? . [ indexOf ( p . x , p . y ) ] && pointInFocus ( p . x , p . y , normalizedFocus ? 26 : 0 ) )
. sort ( ( a , b ) => ( ( b . population || 0 ) + ( b . isPrefecturalCapital ? 300000 : 0 ) ) - ( ( a . population || 0 ) + ( a . isPrefecturalCapital ? 300000 : 0 ) ) ) ;
const result = { targetRatio , focusRect : normalizedFocus , nationalLength : Math . round ( nationalLength ) , beforeRailLength : Math . round ( currentRailLength ( ) ) , targetRailLength : Math . round ( targetLength ) , added : 0 , noTarget : 0 , noPath : 0 } ;
const visited = new Set ( ) ;
for ( const node of candidates ) {
if ( result . added >= 28 || currentRailLength ( ) >= targetLength ) break ;
const key = ` ${ Math . round ( node . x ) } , ${ Math . round ( node . y ) } ` ;
if ( visited . has ( key ) ) continue ;
visited . add ( key ) ;
const railSet = [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) , ... ( features . externalRailways || [ ] ) ] ;
if ( anyPathTouches ( railSet , node , 3.0 ) ) continue ;
let target = nearestPointOnPaths ( railSet , node , 85 ) ;
if ( ! target ) {
target = candidates
. filter ( ( q ) => q !== node && ! visited . has ( ` ${ Math . round ( q . x ) } , ${ Math . round ( q . y ) } ` ) )
. map ( ( q ) => ( { ... q , d : Math . hypot ( q . x - node . x , q . y - node . y ) } ) )
. filter ( ( q ) => q . d >= 10 && q . d <= 95 )
. sort ( ( a , b ) => a . d - b . d ) [ 0 ] || null ;
}
if ( ! target ) { result . noTarget ++ ; continue ; }
const d = Math . hypot ( target . x - node . x , target . y - node . y ) ;
let path = routeTerrainPath ( node , target , terrain , { maxLength : d * 2.55 + 42 , maxSeaRun : 0 , maxTunnelRun : 12 , snapRadius : 2.0 } ) ;
if ( ! path . length ) path = terrainFirstConnector ( node , target , terrain , { skipPrimaryRoute : true , maxLength : d * 2.65 + 38 , maxSeaRun : 0 , maxTunnelRun : 12 , shortFallback : 7 } ) ;
if ( ! path . length || pathLengthCells ( path ) < 4 || ! trunkElevationSafe ( path , terrain , 0.72 ) ) { result . noPath ++ ; continue ; }
path = terrainSafeSmooth ( path , terrain , "rail" , 2 ) ;
if ( ! hardTerrainPathValid ( path , "rail" ) ) { result . noPath ++ ; continue ; }
features . branchRailways . push ( path ) ;
result . added ++ ;
}
// If all important nodes are already within station-distance of a line, the
// simple "unserved node -> nearest rail" pass cannot add urban capacity even
// when the railway network is still much sparser than the national-road
// network. Add a small number of secondary OD corridors between populated
// hubs. These remain full terrain-routed railways and are rejected when the
// interior would merely shadow an existing line.
result . secondaryCorridorsAdded = 0 ;
if ( nationalLength > 0 && currentRailLength ( ) < targetLength * 0.96 ) {
const rawHubs = [
... ( features . modernCities || [ ] ) . filter ( ( p ) => ( p . population || 0 ) >= 18000 ) ,
... ( features . markets || [ ] ) . filter ( ( p ) => ( p . population || 0 ) >= 4500 ) ,
... ( features . newTowns || [ ] ) . filter ( ( p ) => ( p . population || 0 ) >= 5000 ) ,
... ( features . satelliteCities || [ ] ) . filter ( ( p ) => ( p . population || 0 ) >= 7000 ) ,
] . filter ( ( p ) => p && inside ( p . x , p . y ) && ! terrain ? . sea ? . [ indexOf ( p . x , p . y ) ] && pointInFocus ( p . x , p . y , normalizedFocus ? 30 : 0 ) ) ;
const hubs = [ ] ;
for ( const p of rawHubs . sort ( ( a , b ) => ( b . population || 0 ) - ( a . population || 0 ) ) ) {
if ( hubs . every ( ( q ) => Math . hypot ( q . x - p . x , q . y - p . y ) >= 5.5 ) ) hubs . push ( p ) ;
if ( hubs . length >= 34 ) break ;
}
const pairs = [ ] ;
for ( let a = 0 ; a < hubs . length ; a ++ ) for ( let b = a + 1 ; b < hubs . length ; b ++ ) {
const A = hubs [ a ] , B = hubs [ b ] ;
if ( ! sameLandComponent ( A , B ) ) continue ;
const d = Math . hypot ( A . x - B . x , A . y - B . y ) ;
if ( d < 10 || d > 82 ) continue ;
const demand = Math . sqrt ( Math . max ( 3000 , A . population || 0 ) * Math . max ( 3000 , B . population || 0 ) ) ;
const score = d / Math . max ( 0.35 , demand / 90000 + ( A . isPrefecturalCapital || B . isPrefecturalCapital ? 0.45 : 0 ) ) ;
pairs . push ( { A , B , d , score } ) ;
}
pairs . sort ( ( a , b ) => a . score - b . score || a . d - b . d ) ;
for ( const pair of pairs ) {
if ( result . secondaryCorridorsAdded >= 14 || currentRailLength ( ) >= targetLength ) break ;
const railSet = [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) , ... ( features . externalRailways || [ ] ) ] ;
const existingInfluence = rebuildInfluence ( railSet , 2.6 ) ;
let path = routeTerrainPath ( pair . A , pair . B , terrain , { maxLength : pair . d * 2.75 + 54 , maxSeaRun : 0 , maxTunnelRun : 26 , snapRadius : 1.8 , maxExpanded : SIZE } ) ;
if ( ! path . length ) path = terrainFirstConnector ( pair . A , pair . B , terrain , { skipPrimaryRoute : true , maxLength : pair . d * 2.85 + 58 , maxSeaRun : 0 , maxTunnelRun : 26 , shortFallback : 7 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( path . length < 8 || ! trunkElevationSafe ( path , terrain , 0.72 ) ) continue ;
path = terrainSafeSmooth ( path , terrain , "rail" , 3 ) ;
const from = Math . min ( path . length - 1 , Math . max ( 2 , Math . floor ( path . length * 0.16 ) ) ) ;
const to = Math . max ( from + 1 , Math . ceil ( path . length * 0.84 ) ) ;
let nearExisting = 0 , checked = 0 ;
for ( let k = from ; k < to ; k += 2 ) {
const [ x , y ] = path [ k ] ;
if ( ! inside ( x , y ) ) continue ;
checked ++ ;
if ( ( existingInfluence [ indexOf ( x , y ) ] || 0 ) > 0.30 ) nearExisting ++ ;
}
if ( checked && nearExisting / checked > 0.46 ) continue ;
const runs = pathTerrainRuns ( path , terrain ) ;
if ( runs . maxSeaRun > 0 || runs . maxTunnelRun > 26 || ! hardTerrainPathValid ( path , "rail" ) ) continue ;
const majorPair = ( pair . A . population || 0 ) >= 50000 && ( pair . B . population || 0 ) >= 50000 ;
( majorPair ? features . railways : features . branchRailways ) . push ( path ) ;
result . secondaryCorridorsAdded ++ ;
}
}
result . added += result . secondaryCorridorsAdded ;
result . afterRailLength = Math . round ( currentRailLength ( ) ) ;
result . achievedRatio = nationalLength > 0 ? result . afterRailLength / nationalLength : 0 ;
return result ;
}
// If a publishable crop has enough rail only because the national-road network
// is unusually sparse, deleting useful rail is the wrong correction. Grow the
// national network with the same terrain-routed production connectors used by
// the normal generator until rail is modestly below national-road density.
// This routine never emits a straight/simple fallback and rejects corridors
// that merely shadow an existing national road.
function densifyNationalToRailRatio ( targetRailShare = 0.94 , focusRect = null ) {
features . nationalRoads || = [ ] ;
const normalizedFocus = focusRect && [ focusRect . x0 , focusRect . y0 , focusRect . x1 , focusRect . y1 ] . every ( Number . isFinite )
? { x0 : Math . floor ( focusRect . x0 ) , y0 : Math . floor ( focusRect . y0 ) , x1 : Math . ceil ( focusRect . x1 ) , y1 : Math . ceil ( focusRect . y1 ) }
: null ;
const pointInFocus = ( x , y , margin = 0 ) => ! normalizedFocus
|| ( x >= normalizedFocus . x0 - margin && y >= normalizedFocus . y0 - margin && x < normalizedFocus . x1 + margin && y < normalizedFocus . y1 + margin ) ;
const measuredLength = ( path ) => {
if ( ! normalizedFocus ) return pathLengthCells ( path || [ ] ) ;
let len = 0 ;
for ( let k = 1 ; k < ( path ? . length || 0 ) ; k ++ ) {
const a = path [ k - 1 ] , b = path [ k ] ;
const mx = ( a [ 0 ] + b [ 0 ] ) * 0.5 , my = ( a [ 1 ] + b [ 1 ] ) * 0.5 ;
if ( pointInFocus ( mx , my ) ) len += Math . hypot ( b [ 0 ] - a [ 0 ] , b [ 1 ] - a [ 1 ] ) ;
}
return len ;
} ;
const nationalLength = ( ) => ( features . nationalRoads || [ ] ) . reduce ( ( sum , path ) => sum + measuredLength ( path ) , 0 ) ;
const railLength = [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) ] . reduce ( ( sum , path ) => sum + measuredLength ( path ) , 0 ) ;
const requiredNational = targetRailShare > 0 ? railLength / targetRailShare : railLength ;
const result = { targetRailShare , focusRect : normalizedFocus , railLength : Math . round ( railLength ) , beforeNationalLength : Math . round ( nationalLength ( ) ) , requiredNationalLength : Math . round ( requiredNational ) , added : 0 , accessAdded : 0 , secondaryAdded : 0 , noTarget : 0 , noPath : 0 , parallelRejected : 0 } ;
if ( railLength <= 0 || nationalLength ( ) >= requiredNational ) {
result . afterNationalLength = result . beforeNationalLength ;
result . achievedRailShare = result . afterNationalLength > 0 ? railLength / result . afterNationalLength : 0 ;
return result ;
}
const rawNodes = [
... ( features . modernCities || [ ] ) . filter ( ( p ) => ( p . population || 0 ) >= 5000 ) ,
... ( features . markets || [ ] ) . filter ( ( p ) => ( p . population || 0 ) >= 3000 ) ,
... ( features . newTowns || [ ] ) . filter ( ( p ) => ( p . population || 0 ) >= 3500 ) ,
... ( features . satelliteCities || [ ] ) . filter ( ( p ) => ( p . population || 0 ) >= 4500 ) ,
... ( features . adminCenters || [ ] ) . filter ( ( p ) => ( p . municipalityPopulation || p . population || 0 ) >= 2000 || p . isPrefecturalCapital || p . isRegionalCapital ) ,
... ( features . ports || [ ] ) . filter ( ( p ) => ( p . population || 0 ) >= 3000 ) ,
] . filter ( ( p ) => p && inside ( p . x , p . y ) && ! terrain ? . sea ? . [ indexOf ( p . x , p . y ) ] && pointInFocus ( p . x , p . y , normalizedFocus ? 30 : 0 ) ) ;
const nodes = [ ] ;
for ( const p of rawNodes . sort ( ( a , b ) => ( ( b . population || b . municipalityPopulation || 0 ) + ( b . isPrefecturalCapital ? 350000 : 0 ) ) - ( ( a . population || a . municipalityPopulation || 0 ) + ( a . isPrefecturalCapital ? 350000 : 0 ) ) ) ) {
if ( nodes . every ( ( q ) => Math . hypot ( q . x - p . x , q . y - p . y ) >= 4.0 ) ) nodes . push ( p ) ;
if ( nodes . length >= 48 ) break ;
}
const routeNational = ( a , b ) => {
const d = Math . hypot ( b . x - a . x , b . y - a . y ) ;
if ( d < 3 || d > 170 ) return [ ] ;
let path = routeTerrainPath ( a , b , terrain , { maxLength : d * 2.9 + 68 , maxSeaRun : 0 , maxTunnelRun : 20 , snapRadius : 1.9 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! path . length ) path = terrainFirstConnector ( a , b , terrain , { skipPrimaryRoute : true , maxLength : d * 3.05 + 72 , maxSeaRun : 0 , maxTunnelRun : 20 , shortFallback : 7 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! path . length ) path = routeLandConnectedTerrainFallback ( a , b , terrain , { maxLength : SIZE , maxElevation : 0.695 } ) ;
if ( path . length < 4 || ! trunkElevationSafe ( path , terrain , 0.72 ) ) return [ ] ;
return terrainSafeSmooth ( path , terrain , "national" , 2 ) ;
} ;
const hasExcessiveOverlap = ( path , network ) => {
if ( ! path ? . length || ! network ? . length ) return false ;
const influence = rebuildInfluence ( network , 3.4 ) ;
const from = Math . min ( path . length - 1 , Math . max ( 2 , Math . floor ( path . length * 0.14 ) ) ) ;
const to = Math . max ( from + 1 , Math . ceil ( path . length * 0.86 ) ) ;
let checked = 0 , near = 0 ;
for ( let k = from ; k < to ; k += 2 ) {
const [ x , y ] = path [ k ] ;
if ( ! inside ( x , y ) ) continue ;
checked ++ ;
if ( ( influence [ indexOf ( x , y ) ] || 0 ) > 0.30 ) near ++ ;
}
return checked >= 3 && near / checked > 0.38 ;
} ;
// First give medium/small civic centres proper access to the national-road
// network. This adds useful spokes rather than arbitrary line mileage.
for ( const node of nodes ) {
if ( result . added >= 18 || nationalLength ( ) >= requiredNational ) break ;
const network = [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] ;
if ( anyPathTouches ( network , node , 3.0 ) ) continue ;
const component = componentAt ( node ) ;
let targets = nearestPointsOnPaths ( network , node , 120 , 24 , 3 ) . filter ( ( q ) => componentAt ( q ) === component ) . slice ( 0 , 10 ) ;
if ( ! targets . length ) targets = nodes . filter ( ( q ) => q !== node && sameLandComponent ( node , q ) )
. map ( ( q ) => ( { ... q , d : Math . hypot ( q . x - node . x , q . y - node . y ) } ) ) . filter ( ( q ) => q . d >= 8 && q . d <= 100 ) . sort ( ( a , b ) => a . d - b . d ) . slice ( 0 , 8 ) ;
if ( ! targets . length ) { result . noTarget ++ ; continue ; }
let accepted = [ ] ;
let acceptedScore = Infinity ;
const remaining = Math . max ( 0 , requiredNational - nationalLength ( ) ) ;
for ( const target of targets ) {
const candidate = routeNational ( node , target ) ;
if ( ! candidate . length ) continue ;
if ( hasExcessiveOverlap ( candidate , network ) ) { result . parallelRejected ++ ; continue ; }
const trimmed = trimRouteAtExistingNetwork ( candidate , network , 2.7 , 3 ) ;
if ( trimmed . length < 3 ) continue ;
const contribution = measuredLength ( trimmed ) ;
if ( contribution < 2 ) continue ;
// Do not cure a modest rail/national imbalance by adding a huge trunk
// whose only virtue is that it happens to pass an unserved hamlet.
// Prefer a route whose visible contribution is close to the remaining
// production-density deficit.
const maxUseful = Math . max ( 14 , remaining * 1.45 ) ;
if ( contribution > maxUseful ) continue ;
const score = Math . abs ( contribution - Math . max ( 8 , remaining * 0.78 ) ) ;
if ( score < acceptedScore ) { accepted = trimmed ; acceptedScore = score ; }
}
if ( ! accepted . length ) { result . noPath ++ ; continue ; }
features . nationalRoads . push ( accepted ) ;
result . added ++ ; result . accessAdded ++ ;
}
// If every settlement is already close to a national road but the published
// network is still too sparse, add demand-driven secondary OD corridors.
if ( nationalLength ( ) < requiredNational * 0.99 ) {
const pairs = [ ] ;
for ( let a = 0 ; a < nodes . length ; a ++ ) for ( let b = a + 1 ; b < nodes . length ; b ++ ) {
const A = nodes [ a ] , B = nodes [ b ] ;
if ( ! sameLandComponent ( A , B ) ) continue ;
const d = Math . hypot ( A . x - B . x , A . y - B . y ) ;
if ( d < 12 || d > 95 ) continue ;
const popA = Math . max ( 1500 , A . population || A . municipalityPopulation || 0 ) ;
const popB = Math . max ( 1500 , B . population || B . municipalityPopulation || 0 ) ;
const demand = Math . sqrt ( popA * popB ) ;
const score = d / Math . max ( 0.35 , demand / 65000 + ( A . isPrefecturalCapital || B . isPrefecturalCapital ? 0.35 : 0 ) ) ;
pairs . push ( { A , B , d , score } ) ;
}
pairs . sort ( ( a , b ) => a . score - b . score || a . d - b . d ) ;
for ( const pair of pairs ) {
if ( result . added >= 26 || result . secondaryAdded >= 12 || nationalLength ( ) >= requiredNational ) break ;
const network = [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] ;
const candidate = routeNational ( pair . A , pair . B ) ;
if ( ! candidate . length ) continue ;
if ( hasExcessiveOverlap ( candidate , network ) ) { result . parallelRejected ++ ; continue ; }
let path = trimRouteAtExistingNetwork ( candidate , network , 2.7 , 4 ) ;
const contribution = measuredLength ( path ) ;
const remaining = Math . max ( 0 , requiredNational - nationalLength ( ) ) ;
if ( path . length < 6 || contribution < 4 ) continue ;
if ( contribution > Math . max ( 16 , remaining * 1.40 ) ) continue ;
features . nationalRoads . push ( path ) ;
result . added ++ ; result . secondaryAdded ++ ;
}
}
result . afterNationalLength = Math . round ( nationalLength ( ) ) ;
result . achievedRailShare = result . afterNationalLength > 0 ? railLength / result . afterNationalLength : 0 ;
return result ;
}
// Synthetic diameter/grid streets were visually dominant and ignored the
// morphology of the existing local-road generator. Urban street density is
// now increased earlier by the normal terrain-aware local-access algorithm.
debug . urbanStreetMeshAdded = 0 ;
debug . syntheticUrbanStreetMeshDisabled = true ;
const nationalCityDebug = ensureMajorCityNationalRoadLinks ( 50000 ) ;
debug . nationalMajorCityLinksAdded = nationalCityDebug . added ;
debug . nationalMajorCityLinksCovered = nationalCityDebug . covered ;
debug . nationalMajorCityLinksNoTarget = nationalCityDebug . noTarget ;
debug . nationalMajorCityLinksNoPath = nationalCityDebug . noPath ;
const railCityGuaranteeDebug = ensureMajorCityRailLinks ( 50000 ) ;
debug . railMajorCityLinksAdded = railCityGuaranteeDebug . added ;
debug . railMajorCityLinksCovered = railCityGuaranteeDebug . covered ;
debug . railMajorCityLinksNoTarget = railCityGuaranteeDebug . noTarget ;
debug . railMajorCityLinksNoPath = railCityGuaranteeDebug . noPath ;
const railDensityDebug = densifyRailToNationalRatio ( 0.84 ) ;
debug . railDensityTarget = railDensityDebug ;
const nationalTerminusDebug = connectNearbyPathTermini ( [
{ key : 'national' , paths : nationalRoads } ,
{ key : 'external' , paths : externalRoads } ,
] , 'national' , { maxDistance : 20 , maxAdded : 16 , maxTunnelRun : 8 , straightTerrainPenaltyMax : 0.92 } ) ;
debug . nationalTerminusConnectionsAdded = nationalTerminusDebug . added ;
const minorTerminusDebug = connectNearbyPathTermini ( [
{ key : 'minor' , paths : minorRoads } ,
] , 'local' , { maxDistance : 14 , maxAdded : 30 , maxTunnelRun : 6 , straightTerrainPenaltyMax : 1.04 } ) ;
debug . minorTerminusConnectionsAdded = minorTerminusDebug . added ;
function pruneIsolatedRuralLocalRoads ( ) {
const before = minorRoads . length ;
const settlements = [ ... ( features . modernCities || [ ] ) , ... ( features . markets || [ ] ) , ... ( features . villages || [ ] ) , ... ( features . ports || [ ] ) , ... ( adminCenters || [ ] ) ] ;
const trunks = [ ... nationalRoads , ... externalRoads ] ;
const kept = [ ] ;
function endpointConnected ( point , selfIndex ) {
if ( nearestPointOnPaths ( trunks , { x : point [ 0 ] , y : point [ 1 ] } , 2.4 ) ) return true ;
for ( let j = 0 ; j < minorRoads . length ; j ++ ) {
if ( j === selfIndex ) continue ;
if ( pathTouchesCell ( minorRoads [ j ] , point [ 0 ] , point [ 1 ] , 2.2 ) ) return true ;
}
return false ;
}
function servesSettlement ( path ) {
for ( const p of settlements ) {
if ( ! p || ! inside ( p . x , p . y ) ) continue ;
if ( pathTouchesCell ( path , p . x , p . y , 2.0 ) ) return true ;
}
return false ;
}
for ( let idx = 0 ; idx < minorRoads . length ; idx ++ ) {
const path = minorRoads [ idx ] ;
const len = pathLengthCells ( path || [ ] ) ;
if ( ! path ? . length || len >= 18 || servesSettlement ( path ) ) { kept . push ( path ) ; continue ; }
let density = 0 , n = 0 ;
for ( let k = 0 ; k < path . length ; k += 2 ) {
const [ x , y ] = path [ k ] ;
if ( ! inside ( x , y ) ) continue ;
density += features . populationDensity ? . [ indexOf ( x , y ) ] || 0 ;
n ++ ;
}
const avgDensity = n ? density / n : 0 ;
if ( avgDensity >= 0.11 ) { kept . push ( path ) ; continue ; }
const a = path [ 0 ] , b = path [ path . length - 1 ] ;
const connectedA = endpointConnected ( a , idx ) , connectedB = endpointConnected ( b , idx ) ;
if ( connectedA && connectedB ) kept . push ( path ) ;
else if ( len >= 12 && ( connectedA || connectedB ) ) kept . push ( path ) ;
// else: short rural fragment with no destination -> remove.
}
minorRoads . length = 0 ;
minorRoads . push ( ... kept ) ;
return { before , after : minorRoads . length , pruned : before - minorRoads . length } ;
}
debug . ruralLocalDanglingPrune = pruneIsolatedRuralLocalRoads ( ) ;
// r11.7: countryside access is a first-class production requirement. The
// earlier generator can still leave a sparse rural map after pruning, so
// re-run the *terrain router* for municipal offices and small settlements
// that have no usable ordinary road. No synthetic crossbars or straight
// segments are emitted.
const ruralCoverageCandidates = [
... ( adminCenters || [ ] ) . map ( ( p ) => ( { ... p , _priority : 3 } ) ) ,
... ( features . villages || [ ] ) . filter ( ( p ) => ( p . population || 0 ) <= 14000 ) . map ( ( p ) => ( { ... p , _priority : 2 } ) ) ,
... ( features . markets || [ ] ) . filter ( ( p ) => ( p . population || 0 ) <= 18000 ) . map ( ( p ) => ( { ... p , _priority : 1 } ) ) ,
] . filter ( ( p ) => p && inside ( p . x , p . y ) && ! terrain ? . sea ? . [ indexOf ( p . x , p . y ) ] )
. sort ( ( a , b ) => b . _priority - a . _priority || ( b . population || b . municipalityPopulation || 0 ) - ( a . population || a . municipalityPopulation || 0 ) ) ;
function ensureRuralMunicipalRoadCoverage ( visibleOnly = false ) {
const result = { checked : 0 , alreadyServed : 0 , added : 0 , noTarget : 0 , noPath : 0 , visibleOnly } ;
const focus = visibleOnly && initialVisibleCrop ? {
x0 : Math . max ( 0 , Math . floor ( initialVisibleCrop . x0 ) ) , y0 : Math . max ( 0 , Math . floor ( initialVisibleCrop . y0 ) ) ,
x1 : Math . min ( MAP _W , Math . ceil ( initialVisibleCrop . x1 ) ) , y1 : Math . min ( MAP _H , Math . ceil ( initialVisibleCrop . y1 ) ) ,
} : null ;
const inFocus = ( x , y , margin = 0 ) => ! focus || ( x >= focus . x0 + margin && y >= focus . y0 + margin && x < focus . x1 - margin && y < focus . y1 - margin ) ;
const visibleRoadTouch = ( paths , point , radius = 2.4 ) => {
for ( const path of paths || [ ] ) {
let near = false , inward = 0 ;
for ( const q of path || [ ] ) {
if ( ! q || ! inFocus ( q [ 0 ] , q [ 1 ] , 0 ) ) continue ;
inward ++ ;
if ( Math . hypot ( q [ 0 ] - point . x , q [ 1 ] - point . y ) <= radius ) near = true ;
}
if ( near && ( ! focus || inward >= 3 ) ) return true ;
}
return false ;
} ;
const nearestFocusedRoadPoint = ( paths , point , maxDistance = 64 ) => {
let best = null ;
for ( const path of paths || [ ] ) for ( const q of path || [ ] ) {
if ( ! q || ! inFocus ( q [ 0 ] , q [ 1 ] , 2 ) ) continue ;
const d = Math . hypot ( q [ 0 ] - point . x , q [ 1 ] - point . y ) ;
if ( d <= maxDistance && ( ! best || d < best . d ) ) best = { x : q [ 0 ] , y : q [ 1 ] , d } ;
}
return best ;
} ;
const areaScale = Math . max ( 1 , SIZE / ( 258 * 183 ) ) ;
// The candidate population/order is invariant across the full-map and
// visible-core audit stages. Reuse it and only apply the focus predicate.
const candidates = visibleOnly ? ruralCoverageCandidates . filter ( ( p ) => inFocus ( p . x , p . y , 0 ) ) : ruralCoverageCandidates ;
const seen = new Set ( ) ;
const maxChecks = Math . round ( 260 * Math . min ( 2.4 , areaScale ) ) ;
for ( const point of candidates ) {
if ( result . checked >= maxChecks ) break ;
const key = ` ${ Math . round ( point . x ) } , ${ Math . round ( point . y ) } ` ;
if ( seen . has ( key ) ) continue ;
seen . add ( key ) ; result . checked ++ ;
const roads = [ ... minorRoads , ... nationalRoads , ... externalRoads ] ;
if ( ( visibleOnly ? visibleRoadTouch ( roads , point , 2.4 ) : anyPathTouches ( roads , point , 2.4 ) ) ) { result . alreadyServed ++ ; continue ; }
let target = visibleOnly ? nearestFocusedRoadPoint ( roads , point , 72 ) : nearestPointOnPaths ( roads , point , 64 ) ;
if ( ! target ) {
target = civicTransportTargets . filter ( ( q ) => q !== point && sameLandComponent ( point , q ) && ( ! visibleOnly || inFocus ( q . x , q . y , 3 ) ) )
. map ( ( q ) => ( { ... q , d : Math . hypot ( q . x - point . x , q . y - point . y ) } ) )
. filter ( ( q ) => q . d >= 4 && q . d <= 54 )
. sort ( ( a , b ) => a . d - b . d ) [ 0 ] || null ;
}
if ( ! target ) { result . noTarget ++ ; continue ; }
const d = Math . hypot ( target . x - point . x , target . y - point . y ) ;
let path = routeTerrainPath ( point , target , terrain , { maxLength : d * 3.05 + 36 , maxSeaRun : 0 , maxTunnelRun : 8 , snapRadius : 1.4 , maxExpanded : Math . min ( SIZE , Math . max ( 12000 , Math . floor ( d * d * 10 + 6000 ) ) ) , maxElevation : 0.82 } ) ;
if ( ! path . length ) path = terrainFirstConnector ( point , target , terrain , { skipPrimaryRoute : true , maxLength : d * 3.65 + 54 , maxSeaRun : 0 , maxTunnelRun : 9 , maxExpanded : SIZE , maxElevation : 0.82 } ) ;
if ( ! path . length ) path = routeLandConnectedTerrainFallback ( point , target , terrain , { maxLength : Math . min ( SIZE , d * 4.2 + 72 ) , maxElevation : 0.82 } ) ;
if ( path . length < 4 || pathLengthCells ( path ) > d * 4.35 + 78 ) { result . noPath ++ ; continue ; }
minorRoads . push ( terrainSafeSmooth ( path , terrain , "local" , 1 ) ) ;
result . added ++ ;
}
return result ;
}
debug . ruralMunicipalRoadCoverage = { deferredToFinalFullMapStage : true } ;
function runFinalRuralMunicipalCoverageStages ( ) {
// One full hidden-raster pass is sufficient. A second visible-core pass was
// a crop-specific quality guarantee and repeated the same expensive routing
// work; the published crop now inherits the full-map result directly.
const full = ensureRuralMunicipalRoadCoverage ( false ) ;
return { full , visible : null } ;
}
// Build a sparse organic countryside network with the same terrain-aware local
// road solver used elsewhere. This is deliberately *not* a rectilinear mesh:
// each added road connects a real municipal/rural node to another nearby node
// or to an existing road, and every cell is produced by the terrain router.
const ruralDensifyCandidates = [
... ( adminCenters || [ ] ) . filter ( ( p ) => ( p . municipalityPopulation || p . population || 0 ) < 30000 ) ,
... ( features . villages || [ ] ) . filter ( ( p ) => ( p . population || 0 ) < 16000 ) ,
... ( features . markets || [ ] ) . filter ( ( p ) => ( p . population || 0 ) < 12000 ) ,
] . filter ( ( p ) => p && inside ( p . x , p . y ) && ! terrain ? . sea ? . [ indexOf ( p . x , p . y ) ] )
. sort ( ( a , b ) => ( b . municipalityPopulation || b . population || 0 ) - ( a . municipalityPopulation || a . population || 0 ) ) ;
function densifyRuralLocalRoadNetwork ( visibleOnly = false ) {
const focus = visibleOnly && initialVisibleCrop ? {
x0 : Math . max ( 0 , Math . floor ( initialVisibleCrop . x0 ) ) , y0 : Math . max ( 0 , Math . floor ( initialVisibleCrop . y0 ) ) ,
x1 : Math . min ( MAP _W , Math . ceil ( initialVisibleCrop . x1 ) ) , y1 : Math . min ( MAP _H , Math . ceil ( initialVisibleCrop . y1 ) ) ,
} : null ;
const inFocus = ( p , margin = 0 ) => ! focus || ( p . x >= focus . x0 + margin && p . y >= focus . y0 + margin && p . x < focus . x1 - margin && p . y < focus . y1 - margin ) ;
const raw = visibleOnly ? ruralDensifyCandidates . filter ( ( p ) => inFocus ( p , 1 ) ) : ruralDensifyCandidates ;
const nodes = [ ] ;
for ( const p of raw ) {
if ( nodes . every ( ( q ) => Math . hypot ( q . x - p . x , q . y - p . y ) >= 3.2 ) ) nodes . push ( p ) ;
if ( nodes . length >= 260 ) break ;
}
const areaScale = Math . max ( 1 , SIZE / ( 258 * 183 ) ) ;
const targetAdded = Math . min ( Math . round ( 185 * Math . min ( 2.2 , areaScale ) ) , Math . max ( 34 , Math . round ( nodes . length * 1.10 ) ) ) ;
const result = { visibleOnly , nodes : nodes . length , targetAdded , added : 0 , noPath : 0 , skippedDense : 0 } ;
const localNetwork = ( ) => [ ... ( features . minorRoads || [ ] ) , ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] ;
const localDegree = ( node ) => {
let count = 0 ;
for ( const path of localNetwork ( ) ) {
let touched = false ;
for ( const q of path || [ ] ) if ( Math . hypot ( q [ 0 ] - node . x , q [ 1 ] - node . y ) <= 4.2 ) { touched = true ; break ; }
if ( touched && ++ count >= 2 ) break ;
}
return count ;
} ;
for ( const node of nodes ) {
if ( result . added >= targetAdded ) break ;
// A rural seat with two independent nearby approaches is already well served.
if ( localDegree ( node ) >= 4 ) { result . skippedDense ++ ; continue ; }
const sameLand = nodes
. filter ( ( q ) => q !== node && sameLandComponent ( node , q ) )
. map ( ( q ) => ( { ... q , d : Math . hypot ( q . x - node . x , q . y - node . y ) } ) )
. filter ( ( q ) => q . d >= 5 && q . d <= 64 && inFocus ( q , 1 ) )
. sort ( ( a , b ) => a . d - b . d ) ;
const roadTarget = nearestPointOnPaths ( localNetwork ( ) , node , 64 ) ;
const targets = [ ] ;
// Prefer a real nearby settlement so the countryside forms a connected
// dendritic network; use an existing road as a second option.
for ( const q of sameLand . slice ( 0 , 6 ) ) targets . push ( q ) ;
if ( roadTarget && roadTarget . d >= 4 ) targets . push ( roadTarget ) ;
let path = [ ] ;
for ( const target of targets ) {
const d = Math . hypot ( target . x - node . x , target . y - node . y ) ;
let candidate = routeTerrainPath ( node , target , terrain , {
maxLength : d * 3.0 + 34 , maxSeaRun : 0 , maxTunnelRun : 5 , snapRadius : 0.9 ,
maxExpanded : Math . min ( SIZE , Math . max ( 9000 , Math . floor ( d * d * 9 + 4500 ) ) ) , maxElevation : 0.82 ,
} ) ;
if ( ! candidate . length ) candidate = terrainFirstConnector ( node , target , terrain , {
skipPrimaryRoute : true ,
maxLength : d * 3.4 + 44 , maxSeaRun : 0 , maxTunnelRun : 5 , snapRadius : 0.9 , maxExpanded : SIZE , maxElevation : 0.82 ,
} ) ;
if ( candidate . length < 4 ) continue ;
// Do not add a nearly duplicate local road just to satisfy a count.
const influence = rebuildInfluence ( localNetwork ( ) , 2.0 ) ;
let near = 0 , samples = 0 ;
for ( let k = 2 ; k < candidate . length - 2 ; k += 2 ) {
const [ x , y ] = candidate [ k ] ; if ( ! inside ( x , y ) ) continue ; samples ++ ;
if ( ( influence [ indexOf ( x , y ) ] || 0 ) > 0.55 ) near ++ ;
}
if ( samples >= 4 && near / samples > 0.72 ) continue ;
path = candidate ; break ;
}
if ( ! path . length ) { result . noPath ++ ; continue ; }
features . minorRoads || = [ ] ;
features . minorRoads . push ( terrainSafeSmooth ( path , terrain , "local" , 1 ) ) ;
result . added ++ ;
}
return result ;
}
function runFinalRuralDensificationStages ( ) {
// Densify once on the complete hidden raster. Do not rerun the same node
// population solely for the future crop rectangle.
const full = densifyRuralLocalRoadNetwork ( false ) ;
return { full , visible : null } ;
}
const railTerminusDebug = connectNearbyPathTermini ( [
{ key : 'rail' , paths : features . railways || [ ] } ,
{ key : 'externalRail' , paths : features . externalRailways || [ ] } ,
] , 'rail' , { maxDistance : 20 , maxAdded : 10 , maxTunnelRun : 12 , straightTerrainPenaltyMax : 0.94 } ) ;
debug . railTerminusConnectionsAdded = railTerminusDebug . added ;
function ensureInitialOverscanGatewayContinuations ( ) {
const gateways = ( features . externalGateways || [ ] ) . filter ( ( g ) => g ? . initialOverscan && inside ( g . x , g . y ) && ! terrain ? . sea ? . [ indexOf ( g . x , g . y ) ] ) ;
const result = { enabled : gateways . length > 0 , gateways : gateways . length , nationalAdded : 0 , railAdded : 0 , expresswayAdded : 0 , sides : { } } ;
if ( ! gateways . length ) return result ;
const ranked = gateways . slice ( ) . sort ( ( a , b ) => ( ( b . score || 0 ) + Math . min ( 0.6 , ( b . virtualPopulation || 0 ) / 420000 ) ) - ( ( a . score || 0 ) + Math . min ( 0.6 , ( a . virtualPopulation || 0 ) / 420000 ) ) ) ;
for ( const g of ranked ) result . sides [ g . edgeSide || 'unknown' ] = ( result . sides [ g . edgeSide || 'unknown' ] || 0 ) + 1 ;
function connectGateway ( gateway , paths , mode ) {
if ( anyPathTouches ( paths , gateway , mode === 'expressway' ? 2.5 : 1.8 ) ) return [ ] ;
const component = componentAt ( gateway ) ;
const targets = nearestPointsOnPaths ( paths , gateway , Infinity , 30 , 3 ) . filter ( ( q ) => componentAt ( q ) === component ) . slice ( 0 , 12 ) ;
const civicFallback = [ ... ( features . modernCities || [ ] ) , ... ( features . markets || [ ] ) ]
. filter ( ( q ) => q && sameLandComponent ( gateway , q ) )
. sort ( ( a , b ) => Math . hypot ( a . x - gateway . x , a . y - gateway . y ) - Math . hypot ( b . x - gateway . x , b . y - gateway . y ) )
. slice ( 0 , 5 ) ;
const allTargets = targets . length ? targets : civicFallback ;
for ( const target of allTargets ) {
const d = Math . hypot ( target . x - gateway . x , target . y - gateway . y ) ;
if ( d < 2 ) continue ;
const modeOpts = mode === 'expressway'
? { maxSeaRun : 0 , maxTunnelRun : 24 , snapRadius : 2.4 , factor : 3.2 , slack : 82 }
: mode === 'rail'
? { maxSeaRun : 0 , maxTunnelRun : 26 , snapRadius : 2.0 , factor : 3.0 , slack : 70 }
: { maxSeaRun : 0 , maxTunnelRun : 18 , snapRadius : 1.8 , factor : 2.8 , slack : 58 } ;
let path = routeTerrainPath ( gateway , target , terrain , { maxLength : d * modeOpts . factor + modeOpts . slack , maxSeaRun : modeOpts . maxSeaRun , maxTunnelRun : modeOpts . maxTunnelRun , snapRadius : modeOpts . snapRadius , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! path . length ) path = terrainFirstConnector ( gateway , target , terrain , { skipPrimaryRoute : true , maxLength : d * ( modeOpts . factor + 0.2 ) + modeOpts . slack , maxSeaRun : 0 , maxTunnelRun : modeOpts . maxTunnelRun , shortFallback : 7 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! path . length ) path = routeLandConnectedTerrainFallback ( gateway , target , terrain , { maxLength : SIZE , maxElevation : 0.695 } ) ;
if ( path . length >= 4 && ( mode === 'national' || mode === 'rail' || mode === 'expressway' ? trunkElevationSafe ( path , terrain , 0.72 ) : true ) ) {
path = trimRouteAtExistingNetwork ( path , paths , mode === 'expressway' ? 3.0 : 2.5 , 3 ) ;
return terrainSafeSmooth ( path , terrain , mode , mode === 'expressway' ? 4 : 2 ) ;
}
}
return [ ] ;
}
// National roads have the broadest outside demand, rail slightly less, and
// motorways the fewest exits. Select gateways by side first so the visible
// crop behaves like the middle of a larger network rather than a closed box.
const sideBest = [ ] ;
const usedSides = new Set ( ) ;
for ( const g of ranked ) {
if ( ! usedSides . has ( g . edgeSide ) ) { sideBest . push ( g ) ; usedSides . add ( g . edgeSide ) ; }
}
for ( const g of [ ... sideBest , ... ranked ] . slice ( 0 , Math . min ( 6 , gateways . length ) ) ) {
const network = [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] ;
const path = connectGateway ( g , network , 'national' ) ;
if ( path . length ) { features . externalRoads || = [ ] ; features . externalRoads . push ( path ) ; result . nationalAdded ++ ; }
}
const railGateways = [ ... sideBest , ... ranked . filter ( ( g ) => ! sideBest . includes ( g ) ) ] . slice ( 0 , Math . min ( 4 , gateways . length ) ) ;
for ( const g of railGateways ) {
const network = [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) , ... ( features . externalRailways || [ ] ) ] ;
if ( ! network . length ) break ;
const path = connectGateway ( g , network , 'rail' ) ;
if ( path . length ) { features . externalRailways || = [ ] ; features . externalRailways . push ( path ) ; result . railAdded ++ ; }
}
const expressGateways = sideBest . slice ( 0 , Math . min ( 3 , sideBest . length ) ) ;
for ( const g of expressGateways ) {
const network = [ ... ( features . expressways || [ ] ) , ... ( features . externalExpressways || [ ] ) ] ;
if ( ! network . length ) break ;
const path = connectGateway ( g , network , 'expressway' ) ;
if ( path . length ) { features . externalExpressways || = [ ] ; features . externalExpressways . push ( path ) ; result . expresswayAdded ++ ; }
}
return result ;
}
debug . initialOverscanGatewayContinuations = ensureInitialOverscanGatewayContinuations ( ) ;
// Literal hidden-raster generation needs a second kind of continuation:
// routes must cross the *future published crop boundary*, not merely the true
// hidden-world edge. Otherwise the overscan can be fully generated yet still
// have no visible evidence that transport planning considered off-screen OD
// demand. These continuations are normal terrain-routed trunk paths whose
// outside portions are discarded by the final crop.
function ensureInitialVisibleCropContinuations ( ) {
if ( ! initialVisibleCrop || ! [ initialVisibleCrop . x0 , initialVisibleCrop . y0 , initialVisibleCrop . x1 , initialVisibleCrop . y1 ] . every ( Number . isFinite ) ) {
return { enabled : false , reason : "no-visible-crop" } ;
}
const rect = {
x0 : Math . max ( 0 , Math . floor ( initialVisibleCrop . x0 ) ) ,
y0 : Math . max ( 0 , Math . floor ( initialVisibleCrop . y0 ) ) ,
x1 : Math . min ( MAP _W , Math . ceil ( initialVisibleCrop . x1 ) ) ,
y1 : Math . min ( MAP _H , Math . ceil ( initialVisibleCrop . y1 ) ) ,
} ;
const inFocus = ( x , y , margin = 0 ) => x >= rect . x0 - margin && y >= rect . y0 - margin && x < rect . x1 + margin && y < rect . y1 + margin ;
const crossesFocus = ( path ) => {
let inCount = 0 , outCount = 0 ;
for ( const [ x , y ] of path || [ ] ) {
if ( inFocus ( x , y ) ) inCount ++ ; else outCount ++ ;
if ( inCount && outCount ) return true ;
}
return false ;
} ;
function nearestInsidePathPoint ( paths , point , component ) {
let best = null ;
for ( const path of paths || [ ] ) for ( const [ x , y ] of path || [ ] ) {
if ( ! inFocus ( x , y ) || componentAt ( { x , y } ) !== component ) continue ;
const d = Math . hypot ( x - point . x , y - point . y ) ;
if ( ! best || d < best . d ) best = { x , y , d } ;
}
return best ;
}
const outsideLandByComponent = new Map ( ) ;
for ( let y = 0 ; y < MAP _H ; y += 2 ) for ( let x = 0 ; x < MAP _W ; x += 2 ) {
if ( inFocus ( x , y ) ) continue ;
const dx = x < rect . x0 ? rect . x0 - x : x >= rect . x1 ? x - ( rect . x1 - 1 ) : 0 ;
const dy = y < rect . y0 ? rect . y0 - y : y >= rect . y1 ? y - ( rect . y1 - 1 ) : 0 ;
if ( Math . hypot ( dx , dy ) > 56 ) continue ;
const i = indexOf ( x , y ) ;
if ( terrain ? . sea ? . [ i ] ) continue ;
const component = landComponentId [ i ] ;
if ( component < 0 ) continue ;
let list = outsideLandByComponent . get ( component ) ;
if ( ! list ) outsideLandByComponent . set ( component , list = [ ] ) ;
if ( list . length < 900 ) list . push ( { x , y , borderDistance : Math . hypot ( dx , dy ) } ) ;
}
function syntheticOutsideGateway ( paths ) {
let best = null ;
// Search any nearby halo land on the same component, not merely a
// straight normal projection from the crop edge. Real coastlines often
// turn sharply at the viewport edge, so the older straight-out scan could
// falsely conclude that rail/motorway had no off-screen continuation.
for ( const path of paths || [ ] ) for ( let k = 0 ; k < ( path ? . length || 0 ) ; k += 2 ) {
const [ x , y ] = path [ k ] ;
if ( ! inFocus ( x , y ) ) continue ;
const component = componentAt ( { x , y } ) ;
if ( component < 0 ) continue ;
const candidates = outsideLandByComponent . get ( component ) || [ ] ;
for ( const q of candidates ) {
const d = Math . hypot ( q . x - x , q . y - y ) ;
if ( d < 10 || d > 78 ) continue ;
const qi = indexOf ( q . x , q . y ) ;
const score = d + q . borderDistance * 0.20 + ( terrain ? . slope ? . [ qi ] || 0 ) * 6 + ( terrain ? . ridgeField ? . [ qi ] || 0 ) * 5 ;
if ( ! best || score < best . score ) best = { x : q . x , y : q . y , component , score , side : "halo-land" } ;
}
}
return best ;
}
function outsideCivicCandidates ( ) {
return civicTransportTargets . filter ( ( p ) => ! inFocus ( p . x , p . y ) && componentAt ( p ) >= 0 )
. map ( ( p ) => {
const dx = p . x < rect . x0 ? rect . x0 - p . x : p . x >= rect . x1 ? p . x - ( rect . x1 - 1 ) : 0 ;
const dy = p . y < rect . y0 ? rect . y0 - p . y : p . y >= rect . y1 ? p . y - ( rect . y1 - 1 ) : 0 ;
const borderDistance = Math . hypot ( dx , dy ) ;
const pop = Number ( p . population || p . municipalityPopulation || 0 ) ;
return { ... p , borderDistance , demandScore : borderDistance - Math . log1p ( Math . max ( 0 , pop ) ) * 1.8 } ;
} ) . filter ( ( p ) => p . borderDistance <= 64 )
. sort ( ( a , b ) => a . demandScore - b . demandScore ) ;
}
const outsideCivic = outsideCivicCandidates ( ) ;
function standaloneVisibleToHaloPair ( mode ) {
const minPop = mode === "expressway" ? 10000 : mode === "rail" ? 8000 : 5000 ;
const insideNodes = civicTransportTargets . filter ( ( p ) => inFocus ( p . x , p . y ) && componentAt ( p ) >= 0
&& ( Number ( p . population || p . municipalityPopulation || 0 ) >= minPop || p . isPrefecturalCapital || p . isRegionalCapital ) )
. sort ( ( a , b ) => ( ( b . population || b . municipalityPopulation || 0 ) + ( b . isPrefecturalCapital ? 500000 : 0 ) ) - ( ( a . population || a . municipalityPopulation || 0 ) + ( a . isPrefecturalCapital ? 500000 : 0 ) ) ) ;
let best = null ;
for ( const node of insideNodes ) {
const component = componentAt ( node ) ;
const outside = outsideLandByComponent . get ( component ) || [ ] ;
for ( const q of outside ) {
const d = Math . hypot ( q . x - node . x , q . y - node . y ) ;
if ( d < 12 || d > 150 ) continue ;
let anchor = node ;
if ( mode === "expressway" ) {
const suburban = suburbanExpresswayAnchorForCity ( node , q ) ;
if ( suburban ) anchor = suburban ;
}
const score = d - Math . log1p ( Math . max ( 0 , Number ( node . population || node . municipalityPopulation || 0 ) ) ) * 2.0 + q . borderDistance * 0.2 ;
if ( ! best || score < best . score ) best = { gateway : { x : q . x , y : q . y , component } , anchor : { x : anchor . x , y : anchor . y } , score } ;
}
}
return best ;
}
function addCrossings ( paths , mode , desired ) {
const result = { desired , beforeCrossings : ( paths || [ ] ) . filter ( crossesFocus ) . length , added : 0 , noGateway : 0 , noPath : 0 } ;
if ( result . beforeCrossings >= desired || ! ( paths || [ ] ) . length ) return result ;
const used = new Set ( ) ;
while ( result . beforeCrossings + result . added < desired ) {
let gateway = null , anchor = null ;
for ( const candidate of outsideCivic ) {
const key = ` ${ Math . round ( candidate . x ) } , ${ Math . round ( candidate . y ) } ` ;
if ( used . has ( key ) ) continue ;
const component = componentAt ( candidate ) ;
const insidePoint = nearestInsidePathPoint ( paths , candidate , component ) ;
if ( ! insidePoint ) continue ;
gateway = candidate ; anchor = insidePoint ; used . add ( key ) ; break ;
}
if ( ! gateway ) {
gateway = syntheticOutsideGateway ( paths ) ;
if ( gateway ) anchor = nearestInsidePathPoint ( paths , gateway , gateway . component ) ;
}
if ( ! gateway || ! anchor ) {
const standalone = standaloneVisibleToHaloPair ( mode ) ;
if ( standalone ) { gateway = standalone . gateway ; anchor = standalone . anchor ; }
}
if ( ! gateway || ! anchor ) { result . noGateway ++ ; break ; }
const d = Math . hypot ( gateway . x - anchor . x , gateway . y - anchor . y ) ;
const maxTunnelRun = mode === "expressway" ? 28 : mode === "rail" ? 32 : 22 ;
let path = routeTerrainPath ( gateway , anchor , terrain , { maxLength : d * 3.5 + 96 , maxSeaRun : 0 , maxTunnelRun , snapRadius : mode === "expressway" ? 2.6 : 2.1 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! path . length ) path = terrainFirstConnector ( gateway , anchor , terrain , { skipPrimaryRoute : true , maxLength : d * 3.7 + 108 , maxSeaRun : 0 , maxTunnelRun , shortFallback : 7 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! path . length ) path = routeLandConnectedTerrainFallback ( gateway , anchor , terrain , { maxLength : SIZE , maxElevation : 0.695 } ) ;
if ( path . length < 4 || ! crossesFocus ( path ) || ! trunkElevationSafe ( path , terrain , 0.72 ) ) { result . noPath ++ ; break ; }
path = terrainSafeSmooth ( path , terrain , mode , mode === "expressway" ? 3 : 2 ) ;
if ( ! crossesFocus ( path ) ) { result . noPath ++ ; break ; }
paths . push ( path ) ;
result . added ++ ;
}
result . afterCrossings = ( paths || [ ] ) . filter ( crossesFocus ) . length ;
return result ;
}
const national = addCrossings ( features . nationalRoads || ( features . nationalRoads = [ ] ) , "national" , 2 ) ;
const railPaths = [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) ] ;
const rail = addCrossings ( railPaths , "rail" , 1 ) ;
// Preserve main/branch ownership for existing paths and append any new crop
// continuation to the branch layer; it is a regional continuation, not a
// reason to reclassify the whole mainline.
const existingRailObjects = new Set ( [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) ] ) ;
for ( const path of railPaths ) if ( ! existingRailObjects . has ( path ) ) { features . branchRailways || = [ ] ; features . branchRailways . push ( path ) ; }
const expressway = addCrossings ( features . expressways || ( features . expressways = [ ] ) , "expressway" , 1 ) ;
return { enabled : true , rect , national , rail , expressway , contract : "future published crop is treated as an interior window of a larger terrain-routed network" } ;
}
const finalMajorCities = ( features . modernCities || [ ] )
. filter ( ( city ) => city && inside ( city . x , city . y ) && ! terrain ? . sea ? . [ indexOf ( city . x , city . y ) ] && ( ( city . population || 0 ) >= 50000 || city . isPrefecturalCapital || city . isRegionalCapital ) )
. sort ( ( a , b ) => ( b . population || 0 ) - ( a . population || 0 ) ) ;
// A city whose centre survives the literal overscan crop is publishable and
// therefore must retain visible service inside that crop. Whole-hidden-map
// service is insufficient: an edge city can otherwise be "served" only by a
// line that immediately leaves the future viewport and is then discarded by
// cropping. Build a terrain-routed inward continuation for each missing
// hierarchy. The simple/draft pipeline is never used here.
function ensureVisibleCropMajorCityInternalService ( ) {
if ( ! initialVisibleCrop ) return { enabled : false , reason : "no-visible-crop" } ;
const rect = {
x0 : Math . max ( 0 , Math . floor ( initialVisibleCrop . x0 ) ) , y0 : Math . max ( 0 , Math . floor ( initialVisibleCrop . y0 ) ) ,
x1 : Math . min ( MAP _W , Math . ceil ( initialVisibleCrop . x1 ) ) , y1 : Math . min ( MAP _H , Math . ceil ( initialVisibleCrop . y1 ) ) ,
} ;
const inFocus = ( x , y , margin = 0 ) => x >= rect . x0 + margin && y >= rect . y0 + margin && x < rect . x1 - margin && y < rect . y1 - margin ;
const cityInFocus = ( city ) => city && inFocus ( city . x , city . y , 0 ) ;
const visiblePointCount = ( path , margin = 0 ) => ( path || [ ] ) . reduce ( ( n , q ) => n + ( inFocus ( q [ 0 ] , q [ 1 ] , margin ) ? 1 : 0 ) , 0 ) ;
// A line that merely kisses the future crop boundary can disappear when
// splitCroppedPath drops a one-cell fragment. Require a real inward run, not
// just hidden-halo service at the city coordinate.
const ordinaryServiceMatch = ( paths , city , radius = 3.0 ) => {
for ( let pathIndex = 0 ; pathIndex < ( paths ? . length || 0 ) ; pathIndex ++ ) {
const path = paths [ pathIndex ] ;
for ( let k = 0 ; k < ( path ? . length || 0 ) ; k ++ ) {
const q = path [ k ] ;
if ( ! q || Math . hypot ( q [ 0 ] - city . x , q [ 1 ] - city . y ) > radius ) continue ;
// Service must continue inward *locally* from the city. A path that
// touches an edge city, leaves into the hidden halo, then re-enters the
// crop tens of cells away is not a usable visible rail/road approach.
for ( const dir of [ - 1 , 1 ] ) {
let run = 0 , interior = 0 ;
for ( let step = 1 ; step <= 12 ; step ++ ) {
const j = k + dir * step ;
if ( j < 0 || j >= path . length ) break ;
const p = path [ j ] ;
if ( ! p || ! inFocus ( p [ 0 ] , p [ 1 ] , 0 ) ) break ;
run ++ ;
if ( inFocus ( p [ 0 ] , p [ 1 ] , 1 ) ) interior ++ ;
if ( run >= 3 && interior >= 1 ) return { pathIndex , k , dir , local : path . slice ( Math . max ( 0 , k - 4 ) , Math . min ( path . length , k + 5 ) ) } ;
}
}
}
}
return null ;
} ;
const ordinaryServed = ( paths , city , radius = 3.0 ) => ! ! ordinaryServiceMatch ( paths , city , radius ) ;
const expressServed = ( city ) => {
const serviceRadius = Math . max ( 18 , Math . min ( 34 , ( city . urbanRadius || 12 ) * 2.2 ) ) ;
return [ ... ( features . expressways || [ ] ) , ... ( features . externalExpressways || [ ] ) ] . some ( ( path ) => {
let insideCount = 0 , minD = Infinity , maxD = 0 ;
for ( const [ x , y ] of path || [ ] ) {
if ( ! inFocus ( x , y ) ) continue ;
insideCount ++ ;
const d = Math . hypot ( x - city . x , y - city . y ) ;
minD = Math . min ( minD , d ) ; maxD = Math . max ( maxD , d ) ;
}
return insideCount >= 3 && minD <= serviceRadius && maxD - minD >= 2 ;
} ) ;
} ;
function internalNetworkTargets ( paths , city , component , limit = 16 ) {
const rows = [ ] ;
for ( const path of paths || [ ] ) for ( let k = 0 ; k < ( path ? . length || 0 ) ; k += 2 ) {
const [ x , y ] = path [ k ] ;
if ( ! inFocus ( x , y , 3 ) || componentAt ( { x , y } ) !== component ) continue ;
const d = Math . hypot ( x - city . x , y - city . y ) ;
if ( d < 7 || d > 190 ) continue ;
rows . push ( { x , y , d } ) ;
}
return rows . sort ( ( a , b ) => a . d - b . d ) . slice ( 0 , limit ) ;
}
function internalCivicTargets ( city , component , minD = 10 , maxD = 180 , limit = 18 ) {
return civicTransportTargets . filter ( ( q ) => q && q !== city && inFocus ( q . x , q . y , 4 ) && componentAt ( q ) === component )
. map ( ( q ) => ( { ... q , d : Math . hypot ( q . x - city . x , q . y - city . y ) } ) )
. filter ( ( q ) => q . d >= minD && q . d <= maxD )
. sort ( ( a , b ) => {
const ap = Number ( a . population || a . municipalityPopulation || 0 ) + ( a . isPrefecturalCapital ? 200000 : 0 ) ;
const bp = Number ( b . population || b . municipalityPopulation || 0 ) + ( b . isPrefecturalCapital ? 200000 : 0 ) ;
return a . d - b . d || bp - ap ;
} ) . slice ( 0 , limit ) ;
}
function routeInward ( city , targets , mode ) {
for ( const target of targets ) {
const d = Math . hypot ( target . x - city . x , target . y - city . y ) ;
const maxTunnelRun = mode === "rail" ? 30 : 22 ;
let path = routeTerrainPath ( city , target , terrain , { maxLength : d * 3.2 + 86 , maxSeaRun : 0 , maxTunnelRun , snapRadius : 2.0 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! path . length ) path = terrainFirstConnector ( city , target , terrain , { skipPrimaryRoute : true , maxLength : d * 3.3 + 90 , maxSeaRun : 0 , maxTunnelRun , shortFallback : 7 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! path . length ) path = routeLandConnectedTerrainFallback ( city , target , terrain , { maxLength : SIZE , maxElevation : 0.695 } ) ;
if ( path . length < 4 || visiblePointCount ( path ) < 3 || ! trunkElevationSafe ( path , terrain , 0.72 ) ) continue ;
path = terrainSafeSmooth ( path , terrain , mode , 2 ) ;
if ( ! hardTerrainPathValid ( path , mode ) ) continue ;
return path ;
}
return [ ] ;
}
function visibleSuburbanAnchor ( city , hint = null ) {
const component = componentAt ( city ) ;
const inner = Math . max ( 7 , Math . round ( ( city . coreRadius || 4 ) + 5 ) ) ;
const outer = Math . max ( inner + 6 , Math . round ( ( city . urbanRadius || 12 ) * 1.9 ) ) ;
let best = null ;
for ( let dy = - outer ; dy <= outer ; dy ++ ) for ( let dx = - outer ; dx <= outer ; dx ++ ) {
const x = city . x + dx , y = city . y + dy ;
if ( ! inFocus ( x , y , 2 ) || ! inside ( x , y ) ) continue ;
const d = Math . hypot ( dx , dy ) ;
if ( d < inner || d > outer ) continue ;
const i = indexOf ( x , y ) ;
if ( terrain ? . sea ? . [ i ] || landComponentId [ i ] !== component ) continue ;
const centreBias = Math . hypot ( x - ( rect . x0 + rect . x1 ) * 0.5 , y - ( rect . y0 + rect . y1 ) * 0.5 ) * 0.012 ;
const hintBias = hint ? Math . hypot ( x - hint . x , y - hint . y ) * 0.025 : 0 ;
const score = centreBias + hintBias + ( terrain ? . slope ? . [ i ] || 0 ) * 1.1 + ( terrain ? . ridgeField ? . [ i ] || 0 ) * 0.8 ;
if ( ! best || score < best . score ) best = { x , y , score } ;
}
return best ;
}
function interiorLandTarget ( city , component , minD = 18 , maxD = 120 ) {
let best = null ;
const cx = ( rect . x0 + rect . x1 ) * 0.5 , cy = ( rect . y0 + rect . y1 ) * 0.5 ;
for ( let y = rect . y0 + 4 ; y < rect . y1 - 4 ; y += 3 ) for ( let x = rect . x0 + 4 ; x < rect . x1 - 4 ; x += 3 ) {
const i = indexOf ( x , y ) ;
if ( terrain ? . sea ? . [ i ] || landComponentId [ i ] !== component ) continue ;
const d = Math . hypot ( x - city . x , y - city . y ) ;
if ( d < minD || d > maxD ) continue ;
const score = d * 0.16 + Math . hypot ( x - cx , y - cy ) * 0.025 + ( terrain ? . slope ? . [ i ] || 0 ) * 5 + ( terrain ? . ridgeField ? . [ i ] || 0 ) * 4 ;
if ( ! best || score < best . score ) best = { x , y , score } ;
}
return best ;
}
const result = { enabled : true , checked : 0 , nationalAdded : 0 , railAdded : 0 , expresswayAdded : 0 , noPath : 0 , unresolved : [ ] } ;
for ( const city of finalMajorCities . filter ( cityInFocus ) ) {
result . checked ++ ;
const component = componentAt ( city ) ;
const nationalNetwork = [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] ;
if ( ! ordinaryServed ( nationalNetwork , city , 3.0 ) ) {
const targets = [ ... internalNetworkTargets ( nationalNetwork , city , component ) , ... internalCivicTargets ( city , component ) ] ;
const path = routeInward ( city , targets , "national" ) ;
if ( path . length ) { features . nationalRoads || = [ ] ; features . nationalRoads . push ( path ) ; result . nationalAdded ++ ; }
else result . noPath ++ ;
}
const railNetwork = [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) , ... ( features . externalRailways || [ ] ) ] ;
const railServiceMatch = ordinaryServiceMatch ( railNetwork , city , 3.0 ) ;
if ( ! railServiceMatch ) {
const targets = [ ... internalNetworkTargets ( railNetwork , city , component ) , ... internalCivicTargets ( city , component ) ] ;
const path = routeInward ( city , targets , "rail" ) ;
if ( path . length ) { features . branchRailways || = [ ] ; features . branchRailways . push ( path ) ; result . railAdded ++ ; }
else result . noPath ++ ;
}
if ( ! expressServed ( city ) ) {
const expressNetwork = [ ... ( features . expressways || [ ] ) , ... ( features . externalExpressways || [ ] ) ] ;
let targets = internalNetworkTargets ( expressNetwork , city , component , 12 ) ;
if ( ! targets . length ) targets = internalCivicTargets ( city , component , 16 , 170 , 12 ) ;
if ( ! targets . length ) { const q = interiorLandTarget ( city , component ) ; if ( q ) targets = [ q ] ; }
let path = [ ] ;
let pathIsExternal = false ;
const validateExpressCandidate = ( candidate ) => {
if ( candidate . length < 4 || visiblePointCount ( candidate ) < 3 ) return [ ] ;
const terrainRuns = pathTerrainRuns ( candidate , terrain ) ;
const terrainBurden = pathTerrainBurden ( candidate , terrain ) ;
if ( terrainRuns . maxSeaRun > 0 || terrainRuns . maxTunnelRun > 28 || terrainBurden . highBarrierShare > 0.27 ) return [ ] ;
candidate = terrainSafeSmooth ( candidate , terrain , "expressway" , 3 ) ;
const turns = pathSharpTurnStats ( candidate ) ;
if ( turns . consecutiveExtreme > 1 || turns . sharpShare > 0.46 || ! hardTerrainPathValid ( candidate , "expressway" ) ) return [ ] ;
return candidate ;
} ;
for ( const target of targets ) {
const anchor = visibleSuburbanAnchor ( city , target ) ;
if ( ! anchor ) continue ;
let endpoint = target ;
if ( ! inFocus ( endpoint . x , endpoint . y , 2 ) ) { const q = interiorLandTarget ( city , component ) ; if ( ! q ) continue ; endpoint = q ; }
const d = Math . hypot ( anchor . x - endpoint . x , anchor . y - endpoint . y ) ;
if ( d < 7 ) continue ;
let candidate = routeTerrainPath ( anchor , endpoint , terrain , { maxLength : d * 3.35 + 96 , maxSeaRun : 0 , maxTunnelRun : 28 , snapRadius : 2.3 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! candidate . length ) candidate = terrainFirstConnector ( anchor , endpoint , terrain , { skipPrimaryRoute : true , maxLength : d * 3.5 + 104 , maxSeaRun : 0 , maxTunnelRun : 28 , shortFallback : 7 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! candidate . length ) candidate = routeLandConnectedTerrainFallback ( anchor , endpoint , terrain , { maxLength : SIZE } ) ;
candidate = validateExpressCandidate ( candidate ) ;
if ( ! candidate . length ) continue ;
path = candidate ; break ;
}
// If direct OD routing cannot find a legal corridor, follow the city's
// already terrain-valid national-road valley as *waypoints*. This does
// not copy the national geometry: each motorway leg is independently
// solved by the strict terrain router, avoiding both straight chords and
// mountain/sea clipping.
if ( ! path . length ) {
const nationalGuides = [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ]
. map ( ( guide ) => ( { guide , hit : nearestPointOnPaths ( [ guide ] , city , 7 ) } ) )
. filter ( ( row ) => row . hit && componentAt ( row . hit ) === component )
. sort ( ( a , b ) => a . hit . d - b . hit . d ) ;
for ( const row of nationalGuides . slice ( 0 , 5 ) ) {
const guideHintTuple = row . guide [ Math . min ( row . guide . length - 1 , Math . max ( 0 , Math . floor ( row . guide . length * 0.7 ) ) ) ] ;
const guideHint = guideHintTuple ? { x : guideHintTuple [ 0 ] , y : guideHintTuple [ 1 ] } : null ;
const anchor = visibleSuburbanAnchor ( city , guideHint ) ;
let guided = sharedSuburbanTerrainAlignment ( city , row . guide , "expressway" , { desiredLength : 70 , focusRect : rect } ) ;
if ( ! guided . length && anchor ) guided = routeAlongTerrainGuide ( anchor , row . guide , "expressway" , { desiredLength : 76 , stepDistance : 8 } ) ;
if ( ! guided . length && anchor ) guided = sharedTerrainAlignmentFromGuide ( anchor , row . guide , "expressway" , { desiredLength : 66 } ) ;
const candidate = validateExpressCandidate ( guided ) ;
if ( candidate . length >= 4 && visiblePointCount ( candidate ) >= 3 ) { path = candidate ; break ; }
}
}
// A publishable edge city can sit on a peninsula whose land connection
// to the rest of its component lies entirely in the hidden halo. In that
// case an inward motorway is geographically impossible, but the already
// generated off-screen motorway should visibly reach the crop boundary.
// Connect a visible suburban anchor to that hidden same-land network so
// cropping leaves a truthful outward motorway stub instead of erasing
// service altogether.
if ( ! path . length ) {
const hiddenTargets = [ ] ;
for ( const existingPath of expressNetwork ) for ( let k = 0 ; k < ( existingPath ? . length || 0 ) ; k += 2 ) {
const [ x , y ] = existingPath [ k ] ;
if ( inFocus ( x , y ) || componentAt ( { x , y } ) !== component ) continue ;
const d = Math . hypot ( x - city . x , y - city . y ) ;
if ( d < 8 || d > 150 ) continue ;
hiddenTargets . push ( { x , y , d } ) ;
}
hiddenTargets . sort ( ( a , b ) => a . d - b . d ) ;
for ( const target of hiddenTargets . slice ( 0 , 16 ) ) {
const anchor = visibleSuburbanAnchor ( city , target ) ;
if ( ! anchor ) continue ;
const d = Math . hypot ( anchor . x - target . x , anchor . y - target . y ) ;
let candidate = routeTerrainPath ( anchor , target , terrain , { maxLength : d * 3.5 + 108 , maxSeaRun : 0 , maxTunnelRun : 28 , snapRadius : 2.4 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! candidate . length ) candidate = routeLandConnectedTerrainFallback ( anchor , target , terrain , { maxLength : SIZE } ) ;
candidate = validateExpressCandidate ( candidate ) ;
if ( ! candidate . length || candidate . every ( ( [ x , y ] ) => inFocus ( x , y ) ) ) continue ;
path = candidate ; pathIsExternal = true ; break ;
}
}
// Last production fallback: create a meaningful terrain-routed suburban
// motorway corridor toward the interior of the published landmass. This
// is deliberately long enough to be a real trunk segment (not a cosmetic
// two-cell stub) and is used only when neither the visible nor hidden
// existing motorway can be reached legally.
if ( ! path . length ) {
const centreHint = { x : ( rect . x0 + rect . x1 ) * 0.5 , y : ( rect . y0 + rect . y1 ) * 0.5 } ;
const anchor = visibleSuburbanAnchor ( city , centreHint ) ;
if ( anchor ) {
const endpoints = [ ] ;
for ( let y = rect . y0 + 3 ; y < rect . y1 - 3 ; y += 3 ) for ( let x = rect . x0 + 3 ; x < rect . x1 - 3 ; x += 3 ) {
const i = indexOf ( x , y ) ;
if ( terrain ? . sea ? . [ i ] || landComponentId [ i ] !== component ) continue ;
const d = Math . hypot ( x - anchor . x , y - anchor . y ) ;
const cityD = Math . hypot ( x - city . x , y - city . y ) ;
if ( d < 14 || d > 72 || cityD < Math . hypot ( anchor . x - city . x , anchor . y - city . y ) + 7 ) continue ;
const score = d * 0.08 + Math . hypot ( x - centreHint . x , y - centreHint . y ) * 0.018
+ ( terrain ? . slope ? . [ i ] || 0 ) * 4.5 + ( terrain ? . ridgeField ? . [ i ] || 0 ) * 3.5 + ( terrain ? . naturalBarrierScore ? . [ i ] || 0 ) * 4.0 ;
endpoints . push ( { x , y , d , score } ) ;
}
endpoints . sort ( ( a , b ) => a . score - b . score || b . d - a . d ) ;
for ( const endpoint of endpoints . slice ( 0 , 28 ) ) {
let candidate = routeTerrainPath ( anchor , endpoint , terrain , { maxLength : endpoint . d * 3.2 + 88 , maxSeaRun : 0 , maxTunnelRun : 30 , snapRadius : 2.2 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! candidate . length ) candidate = routeLandConnectedTerrainFallback ( anchor , endpoint , terrain , { maxLength : SIZE } ) ;
candidate = validateExpressCandidate ( candidate ) ;
if ( ! candidate . length || pathLengthCells ( candidate ) < 10 ) continue ;
path = candidate ; break ;
}
}
}
if ( path . length ) {
if ( pathIsExternal ) { features . externalExpressways || = [ ] ; features . externalExpressways . push ( path ) ; }
else { features . expressways || = [ ] ; features . expressways . push ( path ) ; }
result . expresswayAdded ++ ;
} else result . noPath ++ ;
}
}
// Re-audit with the exact visible contract before returning diagnostics.
for ( const city of finalMajorCities . filter ( cityInFocus ) ) {
const national = ordinaryServed ( [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] , city , 3.0 ) ;
const rail = ordinaryServed ( [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) , ... ( features . externalRailways || [ ] ) ] , city , 3.0 ) ;
const expressway = expressServed ( city ) ;
const area = componentAt ( city ) >= 0 ? ( landComponentArea [ componentAt ( city ) ] || 0 ) : 0 ;
if ( ! ( national && rail && expressway ) && ! ( area < 96 && national && rail && ! expressway ) ) result . unresolved . push ( { x : city . x , y : city . y , name : city . name , population : city . population || 0 , national , rail , expressway , landComponentArea : area } ) ;
}
return result ;
}
function pruneRedundantExpresswayBranches ( maxRemoved = 4 ) {
const result = { before : ( features . expressways || [ ] ) . length , after : ( features . expressways || [ ] ) . length , removed : 0 , branchNodesBefore : 0 , branchNodesAfter : 0 } ;
if ( ( features . expressways || [ ] ) . length < 3 ) return result ;
function topologyStats ( paths ) {
const neighbors = new Map ( ) ;
const ensure = ( key ) => { let set = neighbors . get ( key ) ; if ( ! set ) neighbors . set ( key , set = new Set ( ) ) ; return set ; } ;
for ( const path of paths || [ ] ) {
for ( let k = 0 ; k < ( path ? . length || 0 ) ; k ++ ) ensure ( ` ${ path [ k ] [ 0 ] } , ${ path [ k ] [ 1 ] } ` ) ;
for ( let k = 1 ; k < ( path ? . length || 0 ) ; k ++ ) {
const a = ` ${ path [ k - 1 ] [ 0 ] } , ${ path [ k - 1 ] [ 1 ] } ` ;
const b = ` ${ path [ k ] [ 0 ] } , ${ path [ k ] [ 1 ] } ` ;
if ( a === b ) continue ;
ensure ( a ) . add ( b ) ; ensure ( b ) . add ( a ) ;
}
}
let branchNodes = 0 ;
for ( const set of neighbors . values ( ) ) if ( set . size >= 3 ) branchNodes ++ ;
let components = 0 ;
const seen = new Set ( ) ;
for ( const key of neighbors . keys ( ) ) {
if ( seen . has ( key ) ) continue ;
components ++ ;
const queue = [ key ] ; seen . add ( key ) ;
for ( let qi = 0 ; qi < queue . length ; qi ++ ) {
for ( const n of neighbors . get ( queue [ qi ] ) || [ ] ) if ( ! seen . has ( n ) ) { seen . add ( n ) ; queue . push ( n ) ; }
}
}
return { branchNodes , components , nodes : neighbors . size } ;
}
const external = features . externalExpressways || [ ] ;
let current = [ ... ( features . expressways || [ ] ) ] ;
let currentStats = topologyStats ( [ ... current , ... external ] ) ;
result . branchNodesBefore = currentStats . branchNodes ;
if ( currentStats . branchNodes <= 1 ) { result . branchNodesAfter = currentStats . branchNodes ; return result ; }
let guard = 0 ;
while ( result . removed < maxRemoved && guard ++ < 24 && current . length >= 3 ) {
const candidates = current . map ( ( path , index ) => {
let uniqueServiceRisk = 0 ;
let served = 0 ;
for ( const city of finalMajorCities ) {
if ( ! pathServesMajorCity ( path , city , "expressway" ) ) continue ;
served ++ ;
const elsewhere = [ ... current . filter ( ( _ , j ) => j !== index ) , ... external ] . some ( ( q ) => pathServesMajorCity ( q , city , "expressway" ) ) ;
if ( ! elsewhere ) uniqueServiceRisk ++ ;
}
return { index , path , served , uniqueServiceRisk , len : pathLengthCells ( path ) } ;
} ) . filter ( ( row ) => row . uniqueServiceRisk === 0 )
. sort ( ( a , b ) => a . served - b . served || a . len - b . len || a . index - b . index ) ;
let removedOne = false ;
for ( const row of candidates ) {
const remaining = current . filter ( ( _ , index ) => index !== row . index ) ;
const combined = [ ... remaining , ... external ] ;
if ( finalMajorCities . some ( ( city ) => ! combined . some ( ( path ) => pathServesMajorCity ( path , city , "expressway" ) ) ) ) continue ;
const stats = topologyStats ( combined ) ;
// Never trade a branch for a disconnected motorway network. Only keep
// a removal when it materially reduces branching and does not increase
// the number of network components.
if ( stats . components > currentStats . components || stats . branchNodes >= currentStats . branchNodes ) continue ;
current = remaining ;
currentStats = stats ;
result . removed ++ ;
removedOne = true ;
break ;
}
if ( ! removedOne ) break ;
}
features . expressways = current ;
result . after = current . length ;
result . branchNodesAfter = currentStats . branchNodes ;
return result ;
}
function capRailDensityToNationalRatio ( maxRatio = 1.08 , focusRect = null ) {
const normalizedFocus = focusRect && [ focusRect . x0 , focusRect . y0 , focusRect . x1 , focusRect . y1 ] . every ( Number . isFinite )
? { x0 : Math . floor ( focusRect . x0 ) , y0 : Math . floor ( focusRect . y0 ) , x1 : Math . ceil ( focusRect . x1 ) , y1 : Math . ceil ( focusRect . y1 ) }
: null ;
const inFocus = ( x , y ) => ! normalizedFocus || ( x >= normalizedFocus . x0 && y >= normalizedFocus . y0 && x < normalizedFocus . x1 && y < normalizedFocus . y1 ) ;
const measuredLength = ( path ) => {
if ( ! normalizedFocus ) return pathLengthCells ( path || [ ] ) ;
let len = 0 ;
for ( let k = 1 ; k < ( path ? . length || 0 ) ; k ++ ) {
const a = path [ k - 1 ] , b = path [ k ] ;
const mx = ( a [ 0 ] + b [ 0 ] ) * 0.5 , my = ( a [ 1 ] + b [ 1 ] ) * 0.5 ;
if ( inFocus ( mx , my ) ) len += Math . hypot ( b [ 0 ] - a [ 0 ] , b [ 1 ] - a [ 1 ] ) ;
}
return len ;
} ;
const crossesFocus = ( path ) => {
if ( ! normalizedFocus ) return false ;
let insideSeen = false , outsideSeen = false ;
for ( const [ x , y ] of path || [ ] ) {
if ( inFocus ( x , y ) ) insideSeen = true ; else outsideSeen = true ;
if ( insideSeen && outsideSeen ) return true ;
}
return false ;
} ;
const nationalLength = ( features . nationalRoads || [ ] ) . reduce ( ( sum , path ) => sum + measuredLength ( path ) , 0 ) ;
let main = [ ... ( features . railways || [ ] ) ] ;
let branch = [ ... ( features . branchRailways || [ ] ) ] ;
const railLength = ( ) => [ ... main , ... branch ] . reduce ( ( sum , path ) => sum + measuredLength ( path ) , 0 ) ;
const target = nationalLength * maxRatio ;
const minimumTarget = nationalLength * ( normalizedFocus ? 0.84 : 0 ) ;
const result = { maxRatio , focusRect : normalizedFocus , nationalLength : Math . round ( nationalLength ) , beforeRailLength : Math . round ( railLength ( ) ) , removed : 0 , removedMain : 0 , removedBranch : 0 } ;
if ( nationalLength <= 0 || railLength ( ) <= target || main . length + branch . length <= 2 ) { result . afterRailLength = result . beforeRailLength ; result . ratio = nationalLength > 0 ? result . afterRailLength / nationalLength : 0 ; return result ; }
let guard = 0 ;
while ( railLength ( ) > target && guard ++ < 64 && main . length + branch . length > 2 ) {
const rows = [
... main . map ( ( path , index ) => ( { path , index , group : 'main' } ) ) ,
... branch . map ( ( path , index ) => ( { path , index , group : 'branch' } ) ) ,
] ;
const serviceCounts = new Int16Array ( finalMajorCities . length ) ;
for ( const row of rows ) {
for ( let ci = 0 ; ci < finalMajorCities . length ; ci ++ ) if ( pathTouchesCell ( row . path , finalMajorCities [ ci ] . x , finalMajorCities [ ci ] . y , 3.0 ) ) serviceCounts [ ci ] ++ ;
}
const crossingCount = normalizedFocus ? rows . filter ( ( row ) => crossesFocus ( row . path ) ) . length : 0 ;
const removable = rows . filter ( ( row ) => {
if ( row . group === 'main' && main . length <= 2 ) return false ;
const contribution = measuredLength ( row . path ) ;
if ( normalizedFocus && contribution <= 0.01 ) return false ;
if ( normalizedFocus && crossingCount <= 1 && crossesFocus ( row . path ) ) return false ;
if ( normalizedFocus && railLength ( ) - contribution < minimumTarget ) return false ;
for ( let ci = 0 ; ci < finalMajorCities . length ; ci ++ ) {
if ( pathTouchesCell ( row . path , finalMajorCities [ ci ] . x , finalMajorCities [ ci ] . y , 3.0 ) && serviceCounts [ ci ] <= 1 ) return false ;
}
return true ;
} ) . map ( ( row ) => {
let served = 0 ;
for ( const city of finalMajorCities ) if ( pathTouchesCell ( row . path , city . x , city . y , 3.0 ) ) served ++ ;
const focusLen = measuredLength ( row . path ) ;
const totalLen = pathLengthCells ( row . path ) ;
// In a visible-focus cap, prefer removing routes that consume the most
// published density while serving no unique city. Whole-map mode keeps
// the previous long-branch preference.
const score = normalizedFocus
? focusLen * 3.0 + ( served === 0 ? 160 : 0 ) + ( row . group === 'branch' ? 45 : 0 ) + totalLen * 0.15
: totalLen * 2.0 + ( served === 0 ? 120 : 0 ) + ( row . group === 'branch' ? 35 : 0 ) ;
return { ... row , totalLen , focusLen , served , score } ;
} ) . sort ( ( a , b ) => b . score - a . score || b . focusLen - a . focusLen || b . totalLen - a . totalLen ) ;
const victim = removable [ 0 ] ;
if ( ! victim ) break ;
if ( victim . group === 'main' ) { main . splice ( victim . index , 1 ) ; result . removedMain ++ ; }
else { branch . splice ( victim . index , 1 ) ; result . removedBranch ++ ; }
result . removed ++ ;
}
features . railways = main ;
features . branchRailways = branch ;
result . afterRailLength = Math . round ( railLength ( ) ) ;
result . ratio = nationalLength > 0 ? result . afterRailLength / nationalLength : 0 ;
return result ;
}
// Run anti-parallel pruning only after every post-admin road has been added.
// Crossings are ignored by the direction test; only sustained side-by-side
// corridors are removed. Paths that uniquely service a major city are kept.
debug . finalExpresswayParallelPrune = pruneFinalParallelPaths ( expressways , "expressway" , finalMajorCities , { radius : 4 , threshold : 0.20 , directionDot : 0.91 , maxParallelRunSamples : 2 } ) ;
debug . finalNationalParallelPrune = pruneFinalParallelPaths ( nationalRoads , "national" , finalMajorCities , { radius : 3 , threshold : 0.26 , directionDot : 0.90 , maxParallelRunSamples : 2 } ) ;
const originalMainRailPaths = new Set ( features . railways || [ ] ) ;
const combinedRailForParallelPrune = [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) ] ;
debug . finalRailParallelPrune = pruneFinalParallelPaths ( combinedRailForParallelPrune , "rail" , finalMajorCities , { radius : 3 , threshold : 0.36 , directionDot : 0.90 , maxParallelRunSamples : 6 } ) ;
features . railways = combinedRailForParallelPrune . filter ( ( path ) => originalMainRailPaths . has ( path ) ) ;
features . branchRailways = combinedRailForParallelPrune . filter ( ( path ) => ! originalMainRailPaths . has ( path ) ) ;
debug . finalRailDensityCap = capRailDensityToNationalRatio ( 1.02 ) ;
for ( let i = 0 ; i < expressways . length ; i ++ ) expressways [ i ] = terrainSafeSmooth ( expressways [ i ] , terrain , "expressway" , 3 ) ;
for ( let i = 0 ; i < nationalRoads . length ; i ++ ) nationalRoads [ i ] = terrainSafeSmooth ( nationalRoads [ i ] , terrain , "national" , 2 ) ;
for ( let i = 0 ; i < ( features . railways || [ ] ) . length ; i ++ ) features . railways [ i ] = terrainSafeSmooth ( features . railways [ i ] , terrain , "rail" , 2 ) ;
for ( let i = 0 ; i < ( features . branchRailways || [ ] ) . length ; i ++ ) features . branchRailways [ i ] = terrainSafeSmooth ( features . branchRailways [ i ] , terrain , "rail" , 2 ) ;
function pruneShortFinalTrunkSegments ( ) {
const result = { expresswayRemoved : 0 , nationalDowngraded : 0 } ;
function uniquelyServes ( path , allPaths , mode ) {
return finalMajorCities . some ( ( city ) => {
const thisServes = mode === "expressway" ? pathServesMajorCity ( path , city , "expressway" ) : anyPathTouches ( [ path ] , city , 3.0 ) ;
if ( ! thisServes ) return false ;
return ! allPaths . some ( ( other ) => other !== path && ( mode === "expressway" ? pathServesMajorCity ( other , city , "expressway" ) : anyPathTouches ( [ other ] , city , 3.0 ) ) ) ;
} ) ;
}
function bridges ( path , others , radius = 2.8 ) {
if ( ! path ? . length || ! others ? . length ) return false ;
const a = { x : path [ 0 ] [ 0 ] , y : path [ 0 ] [ 1 ] } ;
const b0 = path [ path . length - 1 ] ;
const b = { x : b0 [ 0 ] , y : b0 [ 1 ] } ;
return ! ! nearestPointOnPaths ( others , a , radius ) && ! ! nearestPointOnPaths ( others , b , radius ) ;
}
const allExpress = [ ... expressways , ... externalExpressways ] ;
for ( let i = expressways . length - 1 ; i >= 0 ; i -- ) {
const path = expressways [ i ] ;
if ( pathLengthCells ( path ) >= 10 ) continue ;
const others = allExpress . filter ( ( other ) => other !== path ) ;
if ( uniquelyServes ( path , allExpress , "expressway" ) || bridges ( path , others , 3.0 ) ) continue ;
expressways . splice ( i , 1 ) ;
result . expresswayRemoved ++ ;
}
const allNational = [ ... nationalRoads , ... externalRoads ] ;
for ( let i = nationalRoads . length - 1 ; i >= 0 ; i -- ) {
const path = nationalRoads [ i ] ;
if ( pathLengthCells ( path ) >= 12 ) continue ;
const others = allNational . filter ( ( other ) => other !== path ) ;
if ( uniquelyServes ( path , allNational , "national" ) || bridges ( path , others , 2.6 ) ) continue ;
nationalRoads . splice ( i , 1 ) ;
minorRoads . push ( path ) ;
result . nationalDowngraded ++ ;
}
return result ;
}
// Mandatory service audit after pruning. A 270k-class city must not be left
// without any one of the three trunk hierarchies just because a redundant
// route was removed. Narrow-strait allowances remain terrain constrained.
const finalNationalService = ensureMajorCityNationalRoadLinks ( 50000 ) ;
const finalRailService = ensureMajorCityRailLinks ( 50000 ) ;
const finalExpressService = ensureMajorCityExpresswayLinks ( 50000 ) ;
debug . finalMajorCityServiceAudit = {
national : finalNationalService ,
rail : finalRailService ,
expressway : finalExpressService ,
} ;
debug . finalShortTrunkCleanup = pruneShortFinalTrunkSegments ( ) ;
// One final direction-aware pass is safe because unique major-city service is
// protected. This prevents the audit itself from reintroducing a parallel pair.
debug . finalExpresswayParallelPruneAfterService = pruneFinalParallelPaths ( expressways , "expressway" , finalMajorCities , { radius : 4 , threshold : 0.22 , directionDot : 0.91 , maxParallelRunSamples : 2 } ) ;
debug . finalNationalParallelPruneAfterService = pruneFinalParallelPaths ( nationalRoads , "national" , finalMajorCities , { radius : 3 , threshold : 0.28 , directionDot : 0.90 , maxParallelRunSamples : 2 } ) ;
const mainRailAfterService = new Set ( features . railways || [ ] ) ;
const combinedRailAfterService = [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) ] ;
debug . finalRailParallelPruneAfterService = pruneFinalParallelPaths ( combinedRailAfterService , "rail" , finalMajorCities , { radius : 3 , threshold : 0.38 , directionDot : 0.90 , maxParallelRunSamples : 6 } ) ;
features . railways = combinedRailAfterService . filter ( ( path ) => mainRailAfterService . has ( path ) ) ;
features . branchRailways = combinedRailAfterService . filter ( ( path ) => ! mainRailAfterService . has ( path ) ) ;
features . minorRoads = dedupePaths ( minorRoads , 2 ) ;
features . nationalRoads = dedupePaths ( nationalRoads , 1 ) ;
features . externalRoads = dedupePaths ( externalRoads , 1 ) ;
2026-05-28 00:30:09 +09:00
features . expressways = dedupePaths ( expressways , 2 ) ;
features . externalExpressways = dedupePaths ( externalExpressways , 2 ) ;
2026-05-28 02:47:33 +09:00
features . railways = dedupePaths ( features . railways || [ ] , 2 ) ;
2026-08-11 21:51:07 +09:00
features . branchRailways = dedupePaths ( features . branchRailways || [ ] , 2 ) ;
// National-road service additions happen late and can make an earlier rail
// density target stale. Top up only genuinely under-served maps; already
// dense rail networks are left untouched.
const visibleNationalLengthBeforeFinalRail = ( features . nationalRoads || [ ] ) . reduce ( ( sum , path ) => sum + pathLengthCells ( path || [ ] ) , 0 ) ;
const visibleRailLengthBeforeFinalRail = [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) ] . reduce ( ( sum , path ) => sum + pathLengthCells ( path || [ ] ) , 0 ) ;
debug . finalRailDensityBeforeAudit = {
nationalLength : Math . round ( visibleNationalLengthBeforeFinalRail ) ,
railLength : Math . round ( visibleRailLengthBeforeFinalRail ) ,
ratio : visibleNationalLengthBeforeFinalRail > 0 ? visibleRailLengthBeforeFinalRail / visibleNationalLengthBeforeFinalRail : 0 ,
} ;
if ( visibleNationalLengthBeforeFinalRail > 0 && visibleRailLengthBeforeFinalRail / visibleNationalLengthBeforeFinalRail < 0.76 ) {
debug . finalRailDensityTopUp = densifyRailToNationalRatio ( 0.84 ) ;
features . railways = dedupePaths ( features . railways || [ ] , 2 ) ;
features . branchRailways = dedupePaths ( features . branchRailways || [ ] , 2 ) ;
}
if ( initialVisibleCrop ) {
// The literal hidden-raster initial generator must not satisfy the railway
// quota mostly in the discarded halo. Top up the future published centre
// to a slightly-lower-than-national-road density while solving every new
// route against the complete hidden terrain.
debug . initialVisibleCropRailDensityTopUp = densifyRailToNationalRatio ( 0.82 , initialVisibleCrop ) ;
features . railways = dedupePaths ( features . railways || [ ] , 2 ) ;
features . branchRailways = dedupePaths ( features . branchRailways || [ ] , 2 ) ;
}
// Dedupe is allowed to change path ownership, so audit the service contract
// one last time on the exact arrays that will be emitted.
const postDedupeNationalService = ensureMajorCityNationalRoadLinks ( 50000 ) ;
const postDedupeRailService = ensureMajorCityRailLinks ( 50000 ) ;
const postDedupeExpressService = ensureMajorCityExpresswayLinks ( 50000 ) ;
features . nationalRoads = dedupePaths ( features . nationalRoads || [ ] , 1 ) ;
features . expressways = dedupePaths ( features . expressways || [ ] , 2 ) ;
features . railways = dedupePaths ( features . railways || [ ] , 2 ) ;
features . branchRailways = dedupePaths ( features . branchRailways || [ ] , 2 ) ;
if ( features . minorRoads !== minorRoads
|| features . nationalRoads !== nationalRoads
|| features . externalRoads !== externalRoads
|| features . expressways !== expressways
|| features . externalExpressways !== externalExpressways ) {
throw new Error ( "Post-admin transport path normalization must preserve published array identity." ) ;
}
// The last service repair can create a very short hierarchy segment. Run the
// same short-fragment/parallel cleanup once more on the exact emitted trunk
// arrays. A segment that is the sole service for a major city is protected;
// all other tiny national pieces are downgraded to local roads and tiny
// motorway pieces are removed.
debug . finalShortTrunkCleanupAfterService = pruneShortFinalTrunkSegments ( ) ;
// Collapse short and medium same-direction parallel runs onto one physical
// alignment before deciding whether a whole route is redundant. This is more
// robust than the old overlap-only pruning for 1-3 cell separated corridors
// and avoids the characteristic double expressway/national-road ribbons.
debug . finalExpresswaySharedAlignment = collapseParallelCorridorsOntoSharedAlignment ( expressways , "expressway" , finalMajorCities , terrain , { radius : 5 , directionDot : 0.93 , minRunPoints : 3 } ) ;
debug . finalNationalSharedAlignment = collapseParallelCorridorsOntoSharedAlignment ( nationalRoads , "national" , finalMajorCities , terrain , { radius : 4 , directionDot : 0.93 , minRunPoints : 3 } ) ;
debug . finalExpresswayParallelPruneExactOutput = pruneFinalParallelPaths ( expressways , "expressway" , finalMajorCities , { radius : 5 , threshold : 0.16 , directionDot : 0.93 , maxParallelRunSamples : 2 } ) ;
debug . finalNationalParallelPruneExactOutput = pruneFinalParallelPaths ( nationalRoads , "national" , finalMajorCities , { radius : 4 , threshold : 0.20 , directionDot : 0.93 , maxParallelRunSamples : 2 } ) ;
const exactOutputNationalService = ensureMajorCityNationalRoadLinks ( 50000 ) ;
const exactOutputExpressService = ensureMajorCityExpresswayLinks ( 50000 ) ;
debug . exactOutputTrunkServiceRepair = { national : exactOutputNationalService , expressway : exactOutputExpressService } ;
features . nationalRoads = dedupePaths ( features . nationalRoads || [ ] , 1 ) ;
features . expressways = dedupePaths ( features . expressways || [ ] , 2 ) ;
// A service repair can append a new approach beside an existing trunk. Snap
// that approach onto the established corridor once more; unlike pruning this
// preserves the mandatory-city service contract while eliminating visible
// side-by-side lanes represented as separate road paths.
debug . postServiceExpresswaySharedAlignment = collapseParallelCorridorsOntoSharedAlignment ( features . expressways , "expressway" , finalMajorCities , terrain , { radius : 5 , directionDot : 0.93 , minRunPoints : 3 } ) ;
debug . postServiceNationalSharedAlignment = collapseParallelCorridorsOntoSharedAlignment ( features . nationalRoads , "national" , finalMajorCities , terrain , { radius : 4 , directionDot : 0.93 , minRunPoints : 3 } ) ;
debug . finalExpresswayNearDuplicatePrune = pruneNearDuplicateTrunkPaths ( features . expressways , "expressway" , finalMajorCities , { overlapFloor : 0.46 , maxUniqueCells : 12 } ) ;
debug . finalNationalNearDuplicatePrune = pruneNearDuplicateTrunkPaths ( features . nationalRoads , "national" , finalMajorCities , { overlapFloor : 0.50 , maxUniqueCells : 10 } ) ;
debug . finalExpresswayBranchSimplification = pruneRedundantExpresswayBranches ( 4 ) ;
// Branch simplification preserves every major-city service route by
// construction, but dedupe again so shared alignments remain one emitted
// physical corridor where possible.
features . expressways = dedupePaths ( features . expressways || [ ] , 2 ) ;
// Rail should be dense in urbanised regions but remain modestly below the
// national-road network overall. Redundant routes are removed only when all
// major-city rail service remains covered.
debug . finalRailDensityCapAfterTopUp = capRailDensityToNationalRatio ( 0.96 ) ;
const finalRailServiceAfterDensityCap = ensureMajorCityRailLinks ( 50000 ) ;
debug . finalRailServiceAfterDensityCap = finalRailServiceAfterDensityCap ;
features . railways = dedupePaths ( features . railways || [ ] , 2 ) ;
features . branchRailways = dedupePaths ( features . branchRailways || [ ] , 2 ) ;
debug . finalRailDensityCapExactOutput = capRailDensityToNationalRatio ( 0.98 ) ;
const exactOutputRailService = ensureMajorCityRailLinks ( 50000 ) ;
debug . exactOutputRailServiceRepair = exactOutputRailService ;
features . railways = dedupePaths ( features . railways || [ ] , 2 ) ;
features . branchRailways = dedupePaths ( features . branchRailways || [ ] , 2 ) ;
// Visible-core finalizer for literal overscan. The hidden map is only an
// implementation detail: quality contracts must still hold *after* the
// centre is cropped. Do the final service/density/continuation work here,
// while the router can still see the full hidden terrain and off-screen OD
// context. Nothing after this block may lower visible-core trunk quality.
if ( initialVisibleCrop ) {
const serviceBefore = {
national : ensureMajorCityNationalRoadLinks ( 50000 ) ,
rail : ensureMajorCityRailLinks ( 50000 ) ,
expressway : ensureMajorCityExpresswayLinks ( 50000 ) ,
} ;
const railDensityBeforeCrossing = densifyRailToNationalRatio ( 0.82 , initialVisibleCrop ) ;
const cropContinuations = ensureInitialVisibleCropContinuations ( ) ;
features . nationalRoads = dedupePaths ( features . nationalRoads || [ ] , 1 ) ;
features . expressways = dedupePaths ( features . expressways || [ ] , 2 ) ;
features . railways = dedupePaths ( features . railways || [ ] , 2 ) ;
features . branchRailways = dedupePaths ( features . branchRailways || [ ] , 2 ) ;
// Shared physical alignment is preferred to deleting a semantically needed
// route. Use a generous radius matching the reported ~1 km duplication and
// then prune only paths that still duplicate an already-served corridor.
const nationalAlign1 = collapseParallelCorridorsOntoSharedAlignment ( features . nationalRoads , "national" , finalMajorCities , terrain , { radius : 5 , directionDot : 0.90 , minRunPoints : 2 } ) ;
const expressAlign1 = collapseParallelCorridorsOntoSharedAlignment ( features . expressways , "expressway" , finalMajorCities , terrain , { radius : 6 , directionDot : 0.91 , minRunPoints : 2 } ) ;
const nationalPrune = pruneFinalParallelPaths ( features . nationalRoads , "national" , finalMajorCities , { radius : 5 , threshold : 0.16 , directionDot : 0.90 , maxParallelRunSamples : 2 } ) ;
const expressPrune = pruneFinalParallelPaths ( features . expressways , "expressway" , finalMajorCities , { radius : 6 , threshold : 0.14 , directionDot : 0.91 , maxParallelRunSamples : 2 } ) ;
const serviceAfterPrune = {
national : ensureMajorCityNationalRoadLinks ( 50000 ) ,
rail : ensureMajorCityRailLinks ( 50000 ) ,
expressway : ensureMajorCityExpresswayLinks ( 50000 ) ,
} ;
// Mandatory repairs can append a short approach beside a trunk. Snap those
// approaches onto the established alignment, but do not delete the sole
// route serving a major city.
const nationalAlign2 = collapseParallelCorridorsOntoSharedAlignment ( features . nationalRoads , "national" , finalMajorCities , terrain , { radius : 5 , directionDot : 0.90 , minRunPoints : 2 } ) ;
const expressAlign2 = collapseParallelCorridorsOntoSharedAlignment ( features . expressways , "expressway" , finalMajorCities , terrain , { radius : 6 , directionDot : 0.91 , minRunPoints : 2 } ) ;
features . nationalRoads = dedupePaths ( features . nationalRoads || [ ] , 1 ) ;
features . expressways = dedupePaths ( features . expressways || [ ] , 2 ) ;
// Do this *after* all whole-map rail caps. The old order could achieve 0.9x
// density in the future crop and then remove those exact lines while
// optimizing the hidden halo, leaving a 0.57x published network.
const railDensityFinal = densifyRailToNationalRatio ( 0.82 , initialVisibleCrop ) ;
const railDensityVisibleCap = capRailDensityToNationalRatio ( 0.96 , initialVisibleCrop ) ;
const railServiceFinal = ensureMajorCityRailLinks ( 50000 ) ;
const railDensityVisibleCapAfterService = capRailDensityToNationalRatio ( 0.98 , initialVisibleCrop ) ;
features . railways = dedupePaths ( features . railways || [ ] , 2 ) ;
features . branchRailways = dedupePaths ( features . branchRailways || [ ] , 2 ) ;
// A rail-heavy published core is corrected by adding useful, terrain-routed
// national-road OD corridors rather than deleting the rail service that the
// urban network actually needs. This also makes the intended ordering
// national > rail explicit after the hidden-halo crop.
const nationalDensityVisible = densifyNationalToRailRatio ( 0.86 , initialVisibleCrop ) ;
features . nationalRoads = dedupePaths ( features . nationalRoads || [ ] , 1 ) ;
const nationalAlign3 = collapseParallelCorridorsOntoSharedAlignment ( features . nationalRoads , "national" , finalMajorCities , terrain , { radius : 5 , directionDot : 0.90 , minRunPoints : 2 } ) ;
const nationalPruneAfterDensity = pruneFinalParallelPaths ( features . nationalRoads , "national" , finalMajorCities , { radius : 5 , threshold : 0.16 , directionDot : 0.90 , maxParallelRunSamples : 2 } ) ;
const nationalServiceAfterDensity = ensureMajorCityNationalRoadLinks ( 50000 ) ;
const nationalDensityVisibleFinal = densifyNationalToRailRatio ( 0.90 , initialVisibleCrop ) ;
features . nationalRoads = dedupePaths ( features . nationalRoads || [ ] , 1 ) ;
const railDensityVisibleFloorAfterNational = densifyRailToNationalRatio ( 0.90 , initialVisibleCrop ) ;
const railDensityVisibleCapFinal = capRailDensityToNationalRatio ( 0.98 , initialVisibleCrop ) ;
const railServiceAfterNationalDensity = ensureMajorCityRailLinks ( 50000 ) ;
features . railways = dedupePaths ( features . railways || [ ] , 2 ) ;
features . branchRailways = dedupePaths ( features . branchRailways || [ ] , 2 ) ;
// Final publishable-core service is stronger than the hidden-map audit: an
// edge city must have an inward visible national road, railway and motorway
// rather than being served only by a path that disappears into the halo.
const visibleMajorCityInternalService = ensureVisibleCropMajorCityInternalService ( ) ;
features . nationalRoads = dedupePaths ( features . nationalRoads || [ ] , 1 ) ;
features . expressways = dedupePaths ( features . expressways || [ ] , 2 ) ;
features . railways = dedupePaths ( features . railways || [ ] , 2 ) ;
features . branchRailways = dedupePaths ( features . branchRailways || [ ] , 2 ) ;
const nationalAlignAfterVisibleService = collapseParallelCorridorsOntoSharedAlignment ( features . nationalRoads , "national" , finalMajorCities , terrain , { radius : 5 , directionDot : 0.90 , minRunPoints : 2 } ) ;
const expressAlignAfterVisibleService = collapseParallelCorridorsOntoSharedAlignment ( features . expressways , "expressway" , finalMajorCities , terrain , { radius : 6 , directionDot : 0.91 , minRunPoints : 2 } ) ;
const nationalDensityAfterVisibleService = densifyNationalToRailRatio ( 0.86 , initialVisibleCrop ) ;
const railDensityAfterVisibleService = densifyRailToNationalRatio ( 0.90 , initialVisibleCrop ) ;
const railCapAfterVisibleService = capRailDensityToNationalRatio ( 0.98 , initialVisibleCrop ) ;
debug . initialVisibleCropFinalizer = {
serviceBefore , railDensityBeforeCrossing , cropContinuations ,
nationalAlign1 , expressAlign1 , nationalPrune , expressPrune ,
serviceAfterPrune , nationalAlign2 , expressAlign2 , railDensityFinal , railDensityVisibleCap , railServiceFinal , railDensityVisibleCapAfterService ,
nationalDensityVisible , nationalAlign3 , nationalPruneAfterDensity , nationalServiceAfterDensity , nationalDensityVisibleFinal ,
railDensityVisibleFloorAfterNational , railDensityVisibleCapFinal , railServiceAfterNationalDensity ,
visibleMajorCityInternalService , nationalAlignAfterVisibleService , expressAlignAfterVisibleService , nationalDensityAfterVisibleService , railDensityAfterVisibleService , railCapAfterVisibleService ,
productionOnly : true , simplifiedOutputForbidden : true ,
} ;
}
// r11.7 final transport completion. These passes run after the literal
// overscan visible-core finalizer, so they see the exact geometry that will
// be cropped/published while still routing on the hidden production terrain.
function ensureUrbanRailMultiplicity ( ) {
features . railways || = [ ] ; features . branchRailways || = [ ] ;
const result = { checked : 0 , added : 0 , noPath : 0 } ;
const cities = ( features . modernCities || [ ] ) . filter ( ( c ) => c && inside ( c . x , c . y ) && ! terrain ? . sea ? . [ indexOf ( c . x , c . y ) ] && ( c . population || 0 ) >= 50000 )
. sort ( ( a , b ) => ( b . population || 0 ) - ( a . population || 0 ) ) ;
const maxAdded = Math . min ( 24 , Math . max ( 8 , Math . ceil ( cities . length * 0.9 ) ) ) ;
for ( const city of cities ) {
if ( result . added >= maxAdded ) break ;
result . checked ++ ;
const required = ( city . population || 0 ) >= 500000 ? 3 : ( city . population || 0 ) >= 150000 ? 2 : 2 ;
const allRail = ( ) => [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) , ... ( features . externalRailways || [ ] ) ] ;
let servedPaths = allRail ( ) . filter ( ( path ) => pathTouchesCell ( path , city . x , city . y , 4.2 ) ) . length ;
if ( servedPaths >= required ) continue ;
const component = componentAt ( city ) ;
const targets = [
... finalMajorCities . filter ( ( q ) => q !== city && componentAt ( q ) === component ) ,
... sameComponentCivicTargets ( city , { minDistance : 12 , maxDistance : 145 , limit : 22 } ) ,
] . map ( ( q ) => ( { ... q , d : Math . hypot ( q . x - city . x , q . y - city . y ) } ) )
. filter ( ( q ) => q . d >= 12 && q . d <= 145 )
. sort ( ( a , b ) => {
const ap = Number ( a . population || a . municipalityPopulation || 0 ) + ( a . isPrefecturalCapital ? 300000 : 0 ) ;
const bp = Number ( b . population || b . municipalityPopulation || 0 ) + ( b . isPrefecturalCapital ? 300000 : 0 ) ;
return ( a . d / Math . max ( 1 , Math . sqrt ( ap + 4000 ) ) ) - ( b . d / Math . max ( 1 , Math . sqrt ( bp + 4000 ) ) ) ;
} ) ;
const tried = new Set ( ) ;
for ( const target of targets ) {
if ( servedPaths >= required || result . added >= maxAdded ) break ;
const key = ` ${ Math . round ( target . x ) } , ${ Math . round ( target . y ) } ` ;
if ( tried . has ( key ) ) continue ; tried . add ( key ) ;
const d = Math . hypot ( target . x - city . x , target . y - city . y ) ;
let path = routeTerrainPath ( city , target , terrain , { maxLength : d * 3.0 + 66 , maxSeaRun : 0 , maxTunnelRun : 30 , snapRadius : 1.8 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! path . length ) path = terrainFirstConnector ( city , target , terrain , { skipPrimaryRoute : true , maxLength : d * 3.25 + 76 , maxSeaRun : 0 , maxTunnelRun : 30 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( path . length < 8 || ! trunkElevationSafe ( path , terrain , 0.72 ) ) { result . noPath ++ ; continue ; }
const influence = rebuildInfluence ( allRail ( ) , 3.2 ) ;
let near = 0 , sampled = 0 ;
for ( let k = Math . max ( 3 , Math . floor ( path . length * 0.18 ) ) ; k < Math . ceil ( path . length * 0.82 ) ; k += 2 ) {
const [ x , y ] = path [ k ] ; if ( ! inside ( x , y ) ) continue ; sampled ++ ;
if ( ( influence [ indexOf ( x , y ) ] || 0 ) > 0.40 ) near ++ ;
}
if ( sampled >= 4 && near / sampled > 0.58 ) continue ;
path = terrainSafeSmooth ( path , terrain , "rail" , 2 ) ;
( ( city . population || 0 ) >= 150000 && ( target . population || 0 ) >= 50000 ? features . railways : features . branchRailways ) . push ( path ) ;
result . added ++ ; servedPaths ++ ;
}
}
return result ;
}
function ensureExpresswayTerminalContinuity ( ) {
features . expressways || = [ ] ;
const result = { checked : 0 , justified : 0 , connectorsAdded : 0 , unresolved : 0 } ;
const external = features . externalExpressways || [ ] ;
const ordinary = [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) , ... ( features . minorRoads || [ ] ) ] ;
const gateways = features . externalGateways || [ ] ;
const original = [ ... features . expressways ] ;
const additions = [ ] ;
const allOtherPoints = ( self ) => [ ... original , ... external , ... additions ] . filter ( ( p ) => p !== self ) ;
const nearEdge = ( p ) => p . x <= 3 || p . y <= 3 || p . x >= MAP _W - 4 || p . y >= MAP _H - 4 ;
for ( const path of original ) {
if ( ! path || path . length < 8 ) continue ;
for ( const raw of [ path [ 0 ] , path [ path . length - 1 ] ] ) {
const endpoint = { x : raw [ 0 ] , y : raw [ 1 ] } ; result . checked ++ ;
const connectedExpress = nearestPointOnPaths ( allOtherPoints ( path ) , endpoint , 3.0 ) ;
const ordinaryHit = nearestPointOnPaths ( ordinary , endpoint , 10.0 ) ;
const gateway = nearestEntity ( gateways , endpoint , 8.0 ) ;
const cityTerminal = finalMajorCities . some ( ( c ) => Math . hypot ( c . x - endpoint . x , c . y - endpoint . y ) <= Math . max ( 22 , ( c . urbanRadius || 12 ) * 2.1 ) ) ;
const explicitInterchange = ( features . interchanges || [ ] ) . some ( ( ic ) => Math . hypot ( ic . x - endpoint . x , ic . y - endpoint . y ) <= 5.0 ) ;
// Merely approaching an ordinary road used to make an expressway dead end
// "valid". Require a city/gateway or an actual interchange instead.
const legitimateRoadTerminal = ! ! ordinaryHit && explicitInterchange && cityTerminal ;
if ( nearEdge ( endpoint ) || connectedExpress || gateway || cityTerminal || legitimateRoadTerminal ) { result . justified ++ ; continue ; }
let target = nearestPointOnPaths ( allOtherPoints ( path ) , endpoint , 96 ) ;
if ( target && componentAt ( target ) !== componentAt ( endpoint ) ) target = null ;
if ( ! target ) {
target = finalMajorCities . filter ( ( c ) => sameLandComponent ( endpoint , c ) )
. map ( ( c ) => ( { ... c , d : Math . hypot ( c . x - endpoint . x , c . y - endpoint . y ) } ) )
. filter ( ( c ) => c . d >= 14 && c . d <= 110 )
. sort ( ( a , b ) => a . d - b . d ) [ 0 ] || null ;
}
if ( ! target ) { result . unresolved ++ ; continue ; }
const d = Math . hypot ( target . x - endpoint . x , target . y - endpoint . y ) ;
let connector = routeTerrainPath ( endpoint , target , terrain , { maxLength : d * 3.1 + 80 , maxSeaRun : 0 , maxTunnelRun : 28 , snapRadius : 2.2 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( ! connector . length ) connector = terrainFirstConnector ( endpoint , target , terrain , { skipPrimaryRoute : true , maxLength : d * 3.3 + 88 , maxSeaRun : 0 , maxTunnelRun : 28 , maxExpanded : SIZE , maxElevation : 0.695 } ) ;
if ( connector . length < 8 || ! trunkElevationSafe ( connector , terrain , 0.72 ) ) { result . unresolved ++ ; continue ; }
connector = terrainSafeSmooth ( connector , terrain , "expressway" , 3 ) ;
const turns = pathSharpTurnStats ( connector ) ;
if ( turns . consecutiveExtreme > 1 || turns . sharpShare > 0.46 ) { result . unresolved ++ ; continue ; }
additions . push ( connector ) ; result . connectorsAdded ++ ;
}
}
features . expressways . push ( ... additions ) ;
return result ;
}
function rebuildFinalInterchanges ( ) {
const result = { expresswayLength : 0 , target : 0 , added : 0 , accessAdded : 0 , skippedNoRoad : 0 } ;
const ordinary = [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) , ... ( features . minorRoads || [ ] ) ] ;
const newInterchanges = [ ] ;
const newAccess = [ ] ;
const addAt = ( p , source ) => {
if ( ! p || ! inside ( Math . round ( p . x ) , Math . round ( p . y ) ) || terrain ? . sea ? . [ indexOf ( Math . round ( p . x ) , Math . round ( p . y ) ) ] ) return false ;
if ( newInterchanges . some ( ( ic ) => Math . hypot ( ic . x - p . x , ic . y - p . y ) < 9.0 ) ) return false ;
const hit = nearestPointOnPaths ( ordinary , p , 38 ) ;
if ( ! hit ) { result . skippedNoRoad ++ ; return false ; }
const d = Math . hypot ( hit . x - p . x , hit . y - p . y ) ;
let access = [ ] ;
if ( d > 1.4 ) access = terrainFirstConnector ( p , hit , terrain , { maxLength : d * 2.7 + 22 , maxSeaRun : 0 , maxTunnelRun : 7 , maxExpanded : Math . min ( SIZE , Math . max ( 8000 , Math . floor ( d * d * 8 + 4500 ) ) ) , maxElevation : 0.86 } ) ;
if ( d > 1.4 && access . length < 2 ) { result . skippedNoRoad ++ ; return false ; }
newInterchanges . push ( { x : Math . round ( p . x ) , y : Math . round ( p . y ) , kind : "Interchange" , score : 1 , source } ) ;
if ( access . length >= 2 ) { newAccess . push ( access ) ; features . minorRoads || = [ ] ; features . minorRoads . push ( access ) ; result . accessAdded ++ ; }
result . added ++ ; return true ;
} ;
for ( const path of [ ... ( features . expressways || [ ] ) , ... ( features . externalExpressways || [ ] ) ] ) {
if ( ! path || path . length < 4 ) continue ;
const cum = [ 0 ] ;
for ( let k = 1 ; k < path . length ; k ++ ) cum . push ( cum [ cum . length - 1 ] + Math . hypot ( path [ k ] [ 0 ] - path [ k - 1 ] [ 0 ] , path [ k ] [ 1 ] - path [ k - 1 ] [ 1 ] ) ) ;
const total = cum [ cum . length - 1 ] || 0 ; result . expresswayLength += total ;
const first = { x : path [ 0 ] [ 0 ] , y : path [ 0 ] [ 1 ] } ;
const lastTuple = path [ path . length - 1 ] ;
const last = { x : lastTuple [ 0 ] , y : lastTuple [ 1 ] } ;
// Even a retained short regional spur needs a real terminal IC. Previously
// the early `total < 8` return left such stubs visually ending in mid-road.
if ( total < 8 ) {
addAt ( first , "final-terminal-ic" ) ;
addAt ( last , "final-terminal-ic" ) ;
continue ;
}
let expectedSlots = 0 ;
for ( let s = 5 ; s <= total - 4 ; ) {
let probeK = 1 ; while ( probeK < cum . length && cum [ probeK ] < s ) probeK ++ ;
const probeA = path [ Math . max ( 0 , probeK - 1 ) ] , probeB = path [ Math . min ( path . length - 1 , probeK ) ] ;
const probeSpan = Math . max ( 0.001 , cum [ probeK ] - cum [ probeK - 1 ] ) ;
const probeT = Math . max ( 0 , Math . min ( 1 , ( s - cum [ probeK - 1 ] ) / probeSpan ) ) ;
const probe = { x : Math . round ( probeA [ 0 ] + ( probeB [ 0 ] - probeA [ 0 ] ) * probeT ) , y : Math . round ( probeA [ 1 ] + ( probeB [ 1 ] - probeA [ 1 ] ) * probeT ) } ;
const probeDensity = inside ( probe . x , probe . y ) ? Math . max ( 0 , features . populationDensity ? . [ indexOf ( probe . x , probe . y ) ] || 0 ) : 0 ;
let cityDemand = 0 ;
for ( const city of features . modernCities || [ ] ) {
if ( ! city || ( city . population || 0 ) < 18000 ) continue ;
const radius = Math . max ( 16 , Math . min ( 38 , ( city . urbanRadius || 10 ) * 2.4 ) ) ;
const dCity = Math . hypot ( city . x - probe . x , city . y - probe . y ) ;
if ( dCity > radius ) continue ;
const popWeight = Math . min ( 1 , Math . log10 ( Math . max ( 20000 , city . population || 0 ) / 20000 ) / 1.25 ) ;
cityDemand = Math . max ( cityDemand , ( 1 - dCity / radius ) * ( 0.45 + popWeight * 0.55 ) ) ;
}
const icDemand = Math . max ( Math . min ( 1 , probeDensity * 1.7 ) , cityDemand ) ;
const dynamicGap = icDemand >= 0.72 ? 11.5 : icDemand >= 0.42 ? 14.5 : icDemand >= 0.18 ? 20.5 : 28.5 ;
let best = null ;
for ( let offset = - 4 ; offset <= 4 ; offset += 1.5 ) {
const d0 = Math . max ( 2 , Math . min ( total - 2 , s + offset ) ) ;
let k = 1 ; while ( k < cum . length && cum [ k ] < d0 ) k ++ ;
const a = path [ Math . max ( 0 , k - 1 ) ] , b = path [ Math . min ( path . length - 1 , k ) ] ;
const span = Math . max ( 0.001 , cum [ k ] - cum [ k - 1 ] ) ; const t = Math . max ( 0 , Math . min ( 1 , ( d0 - cum [ k - 1 ] ) / span ) ) ;
const p = { x : Math . round ( a [ 0 ] + ( b [ 0 ] - a [ 0 ] ) * t ) , y : Math . round ( a [ 1 ] + ( b [ 1 ] - a [ 1 ] ) * t ) } ;
const hit = nearestPointOnPaths ( ordinary , p , 34 ) ;
if ( ! hit ) continue ;
const roadD = Math . hypot ( hit . x - p . x , hit . y - p . y ) ;
const density = features . populationDensity ? . [ indexOf ( p . x , p . y ) ] || 0 ;
const score = roadD - density * 12 + Math . abs ( offset ) * 0.12 ;
if ( ! best || score < best . score ) best = { ... p , score } ;
}
if ( best && addAt ( best , "final-density-ic" ) ) expectedSlots ++ ;
s += dynamicGap ;
}
result . target += expectedSlots ;
addAt ( first , "final-terminal-ic" ) ; addAt ( last , "final-terminal-ic" ) ;
}
result . legacyUniformTarget = Math . max ( 0 , Math . round ( result . expresswayLength / 19.5 ) ) ;
result . target = Math . max ( result . target , newInterchanges . length ) ;
interchanges . length = 0 ;
interchanges . push ( ... newInterchanges ) ;
features . icAccessRoads = newAccess ;
return result ;
}
// Exact visual/topological junction repair. The graph auditors previously
// accepted endpoints that were merely within a few cells of another route,
// which looked like a torn road on the rendered map. Turn those near misses
// into real shared-raster junctions using the terrain router only.
function snapNearMissEndpoints ( sourcePaths , targetPaths , mode , options = { } ) {
const result = { checked : 0 , added : 0 , unresolved : 0 } ;
const maxGap = options . maxGap ? ? 3.4 ;
const maxAdded = options . maxAdded ? ? 48 ;
const minGap = options . minGap ? ? 0.75 ;
const originals = [ ... ( sourcePaths || [ ] ) ] ;
const targets = [ ... ( targetPaths || [ ] ) ] ;
const used = new Set ( ) ;
function nearestOtherPoint ( endpoint , selfPath ) {
let best = null ;
for ( const path of targets ) {
if ( ! path || path === selfPath ) continue ;
for ( let k = 0 ; k < path . length ; k ++ ) {
const q = path [ k ] ;
if ( ! q ) continue ;
const d = Math . hypot ( endpoint . x - q [ 0 ] , endpoint . y - q [ 1 ] ) ;
if ( d <= minGap ) return { connected : true , x : q [ 0 ] , y : q [ 1 ] , d } ;
if ( d > maxGap || ( best && d >= best . d ) ) continue ;
best = { connected : false , x : q [ 0 ] , y : q [ 1 ] , d } ;
}
}
return best ;
}
for ( let pi = 0 ; pi < originals . length && result . added < maxAdded ; pi ++ ) {
const path = originals [ pi ] ;
if ( ! path || path . length < 2 ) continue ;
const endpoints = [ path [ 0 ] , path [ path . length - 1 ] ] ;
for ( let ei = 0 ; ei < endpoints . length && result . added < maxAdded ; ei ++ ) {
const raw = endpoints [ ei ] ;
const endpoint = { x : raw [ 0 ] , y : raw [ 1 ] } ;
const id = ` ${ Math . round ( endpoint . x ) } , ${ Math . round ( endpoint . y ) } : ${ mode } ` ;
if ( used . has ( id ) ) continue ;
used . add ( id ) ;
result . checked ++ ;
const hit = nearestOtherPoint ( endpoint , path ) ;
if ( ! hit || hit . connected ) continue ;
const connector = terrainFirstConnector ( endpoint , hit , terrain , {
maxLength : Math . max ( 8 , hit . d * 3.2 + 8 ) ,
maxSeaRun : 0 ,
maxTunnelRun : mode === "local" ? 3 : 0 ,
maxElevation : mode === "local" ? 0.82 : 0.695 ,
snapRadius : 0.45 ,
maxExpanded : Math . min ( SIZE , Math . max ( 4500 , Math . floor ( hit . d * hit . d * 120 + 2500 ) ) ) ,
} ) ;
if ( connector . length < 2 ) { result . unresolved ++ ; continue ; }
const burden = pathTerrainBurden ( connector , terrain ) ;
if ( mode !== "local" && ( burden . highBarrierShare > 0.02 || ! hardTerrainPathValid ( connector , mode ) ) ) { result . unresolved ++ ; continue ; }
// Weld into the original polyline so a visual/topological junction is a
// single exact raster chain instead of another two-ended fragment.
const merged = ei === 0
? [ ... connector . slice ( ) . reverse ( ) , ... path . slice ( 1 ) ]
: [ ... path , ... connector . slice ( 1 ) ] ;
path . length = 0 ;
path . push ( ... merged ) ;
if ( ! targets . includes ( path ) ) targets . push ( path ) ;
result . added ++ ;
}
}
return result ;
}
function hardTerrainPathValid ( path , mode ) {
if ( ! path || path . length < 2 ) return false ;
const maxElevation = mode === "local" ? 0.84 : 0.695 ;
const maxBarrier = mode === "local" ? 0.94 : 0.82 ;
const maxSlope = mode === "local" ? 0.68 : 0.52 ;
const maxRidge = mode === "local" ? 0.90 : 0.62 ;
const passField = terrain ? . passSuitability ;
let samples = 0 ;
let rugged = 0 ;
let slopeSum = 0 ;
let elevationSum = 0 ;
let valleyPassSum = 0 ;
let totalLength = 0 ;
for ( let k = 1 ; k < path . length ; k ++ ) {
const a = path [ k - 1 ] , b = path [ k ] ;
const segLen = Math . hypot ( b [ 0 ] - a [ 0 ] , b [ 1 ] - a [ 1 ] ) ;
totalLength += segLen ;
const steps = Math . max ( 1 , Math . ceil ( segLen ) ) ;
for ( let q = 0 ; q <= steps ; q ++ ) {
const t = q / steps ;
const x = Math . round ( a [ 0 ] + ( b [ 0 ] - a [ 0 ] ) * t ) , y = Math . round ( a [ 1 ] + ( b [ 1 ] - a [ 1 ] ) * t ) ;
if ( ! inside ( x , y ) ) return false ;
const i = indexOf ( x , y ) ;
if ( terrain ? . sea ? . [ i ] ) return false ;
const elev = terrain ? . elevation ? . [ i ] || 0 ;
const barrier = terrain ? . naturalBarrierScore ? . [ i ] || 0 ;
const slopeV = terrain ? . slope ? . [ i ] || 0 ;
const ridgeV = terrain ? . ridgeField ? . [ i ] || 0 ;
const pass = passField ? . [ i ] || 0 ;
const valley = terrain ? . valleyField ? . [ i ] || 0 ;
if ( elev >= maxElevation ) return false ;
if ( mode !== "local" && elev >= 0.66 && pass < 0.50 ) return false ;
if ( ( slopeV > maxSlope || ( ridgeV > maxRidge && elev >= 0.58 ) || ( barrier > maxBarrier && elev >= 0.60 ) ) && pass < ( mode === "local" ? 0.40 : 0.46 ) ) return false ;
if ( q > 0 ) {
const px = Math . round ( a [ 0 ] + ( b [ 0 ] - a [ 0 ] ) * ( ( q - 1 ) / steps ) ) ;
const py = Math . round ( a [ 1 ] + ( b [ 1 ] - a [ 1 ] ) * ( ( q - 1 ) / steps ) ) ;
if ( inside ( px , py ) ) {
const grade = Math . abs ( elev - ( terrain ? . elevation ? . [ indexOf ( px , py ) ] || 0 ) ) ;
if ( mode !== "local" && grade >= 0.10 && pass < 0.46 ) return false ;
}
}
samples ++ ;
slopeSum += slopeV ;
elevationSum += elev ;
valleyPassSum += Math . max ( valley , pass ) ;
if ( mode !== "local" && ( slopeV >= 0.22 || elev >= 0.58 || ( ridgeV >= 0.44 && elev >= 0.50 ) || ( barrier >= 0.58 && elev >= 0.50 ) ) ) rugged ++ ;
}
}
// A route can have adjacent vertices and still be an implausible almost
// straight chord. Reject that geometry only when the traversed corridor is
// genuinely rugged; straight roads across plains remain perfectly valid.
if ( mode !== "local" && samples > 0 && totalLength >= 18 ) {
const first = path [ 0 ] , last = path [ path . length - 1 ] ;
const direct = Math . hypot ( last [ 0 ] - first [ 0 ] , last [ 1 ] - first [ 1 ] ) ;
const straightness = direct / Math . max ( 1 , totalLength ) ;
const ruggedShare = rugged / samples ;
const meanSlope = slopeSum / samples ;
const meanElevation = elevationSum / samples ;
const meanValleyPass = valleyPassSum / samples ;
if ( straightness > 0.90 && ruggedShare > 0.18 && meanValleyPass < 0.46 ) return false ;
if ( totalLength >= 30 && straightness > 0.84 && ruggedShare > 0.30 && ( meanSlope > 0.14 || meanElevation > 0.53 ) && meanValleyPass < 0.50 ) return false ;
// Compare the emitted route with its geometric chord. A road can avoid
// every forbidden cell by one-cell wiggles and still look like a ruler
// line across a mountain range. If the direct chord is materially hostile,
// a production trunk must make a visible terrain-driven detour or achieve
// a substantially better terrain burden than that chord.
const chordSteps = Math . max ( 1 , Math . ceil ( direct ) ) ;
let chordSamples = 0 , chordHard = 0 , chordBurden = 0 ;
let routeBurden = 0 ;
for ( let k = 0 ; k < path . length ; k ++ ) {
const x = Math . round ( path [ k ] [ 0 ] ) , y = Math . round ( path [ k ] [ 1 ] ) ;
if ( ! inside ( x , y ) ) continue ;
const i = indexOf ( x , y ) ;
const e = terrain ? . elevation ? . [ i ] || 0 , sV = terrain ? . slope ? . [ i ] || 0 ;
const rV = terrain ? . ridgeField ? . [ i ] || 0 , bV = terrain ? . naturalBarrierScore ? . [ i ] || 0 ;
const vV = terrain ? . valleyField ? . [ i ] || 0 , pV = passField ? . [ i ] || 0 ;
routeBurden += Math . max ( 0 , sV * 3.2 + rV * 2.0 + bV * 2.4 + Math . max ( 0 , e - 0.48 ) * 6.0 - vV * 1.25 - pV * 1.55 ) ;
}
routeBurden /= Math . max ( 1 , path . length ) ;
for ( let q = 0 ; q <= chordSteps ; q ++ ) {
const t = q / chordSteps ;
const x = Math . round ( first [ 0 ] + ( last [ 0 ] - first [ 0 ] ) * t ) ;
const y = Math . round ( first [ 1 ] + ( last [ 1 ] - first [ 1 ] ) * t ) ;
if ( ! inside ( x , y ) ) { chordHard ++ ; chordSamples ++ ; continue ; }
const i = indexOf ( x , y ) ;
const e = terrain ? . elevation ? . [ i ] || 0 , sV = terrain ? . slope ? . [ i ] || 0 ;
const rV = terrain ? . ridgeField ? . [ i ] || 0 , bV = terrain ? . naturalBarrierScore ? . [ i ] || 0 ;
const vV = terrain ? . valleyField ? . [ i ] || 0 , pV = passField ? . [ i ] || 0 ;
const seaV = ! ! terrain ? . sea ? . [ i ] ;
const hostile = seaV || ( e >= 0.66 && pV < 0.50 ) || ( ( sV >= 0.42 || ( rV >= 0.58 && e >= 0.54 ) || ( bV >= 0.78 && e >= 0.56 ) ) && pV < 0.46 ) ;
if ( hostile ) chordHard ++ ;
chordBurden += seaV ? 12 : Math . max ( 0 , sV * 3.2 + rV * 2.0 + bV * 2.4 + Math . max ( 0 , e - 0.48 ) * 6.0 - vV * 1.25 - pV * 1.55 ) ;
chordSamples ++ ;
}
const chordHardShare = chordHard / Math . max ( 1 , chordSamples ) ;
const meanChordBurden = chordBurden / Math . max ( 1 , chordSamples ) ;
if ( direct >= 18 && chordHardShare >= 0.08 && straightness > 0.90 ) return false ;
if ( direct >= 24 && meanChordBurden > routeBurden * 1.30 + 0.18 && straightness > 0.91 ) return false ;
}
return true ;
}
function removeTerrainInvalidTransportPaths ( ) {
const result = { } ;
const specs = [
[ "minorRoads" , "local" ] , [ "nationalRoads" , "national" ] , [ "ringRoads" , "national" ] , [ "externalRoads" , "national" ] ,
[ "expressways" , "expressway" ] , [ "ringExpressways" , "expressway" ] , [ "externalExpressways" , "expressway" ] ,
[ "railways" , "rail" ] , [ "branchRailways" , "rail" ] , [ "ringRailways" , "rail" ] , [ "externalRailways" , "rail" ] ,
] ;
for ( const [ key , mode ] of specs ) {
const before = ( features [ key ] || [ ] ) . length ;
features [ key ] = ( features [ key ] || [ ] ) . filter ( ( path ) => hardTerrainPathValid ( path , mode ) ) ;
result [ key ] = { before , after : features [ key ] . length , removed : before - features [ key ] . length } ;
}
return result ;
}
// Hard production invariant: no emitted transport polyline may contain a
// sparse vertex jump that the renderer would turn into an artificial straight
// chord. This is deliberately destructive rather than interpolating the gap:
// interpolation would still ignore terrain. Mandatory service is rebuilt below
// with the full terrain router.
function removeDiscontinuousTransportPaths ( maxGap = 2.25 ) {
const result = { } ;
const filter = ( key ) => {
const before = ( features [ key ] || [ ] ) . length ;
features [ key ] = ( features [ key ] || [ ] ) . filter ( ( path ) => path ? . length >= 2 && pathMaxVertexGap ( path ) <= maxGap ) ;
result [ key ] = { before , after : features [ key ] . length , removed : before - features [ key ] . length } ;
} ;
for ( const key of [ "minorRoads" , "nationalRoads" , "ringRoads" , "externalRoads" , "expressways" , "ringExpressways" , "externalExpressways" , "railways" , "branchRailways" , "ringRailways" , "externalRailways" ] ) filter ( key ) ;
return result ;
}
debug . finalDiscontinuousTransportCleanup = removeDiscontinuousTransportPaths ( 2.25 ) ;
debug . finalTerrainInvalidTransportCleanup = removeTerrainInvalidTransportPaths ( ) ;
debug . finalServiceAfterDiscontinuityCleanup = {
national : ensureMajorCityNationalRoadLinks ( 50000 ) ,
rail : ensureMajorCityRailLinks ( 50000 ) ,
expressway : ensureMajorCityExpresswayLinks ( 50000 ) ,
} ;
const railJunctionTarget = [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) , ... ( features . externalRailways || [ ] ) ] ;
debug . finalNearMissJunctionRepair = {
national : snapNearMissEndpoints ( features . nationalRoads || [ ] , [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] , "national" , { maxGap : 3.6 , maxAdded : 56 } ) ,
expressway : snapNearMissEndpoints ( features . expressways || [ ] , [ ... ( features . expressways || [ ] ) , ... ( features . externalExpressways || [ ] ) ] , "expressway" , { maxGap : 4.2 , maxAdded : 28 } ) ,
rail : snapNearMissEndpoints ( [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) ] , railJunctionTarget , "rail" , { maxGap : 3.6 , maxAdded : 48 } ) ,
local : snapNearMissEndpoints ( features . minorRoads || [ ] , [ ... ( features . minorRoads || [ ] ) , ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] , "local" , { maxGap : 2.8 , maxAdded : 80 } ) ,
} ;
features . minorRoads = dedupePaths ( features . minorRoads || [ ] , 1 ) ;
features . nationalRoads = dedupePaths ( features . nationalRoads || [ ] , 1 ) ;
features . expressways = dedupePaths ( features . expressways || [ ] , 1 ) ;
features . railways = dedupePaths ( features . railways || [ ] , 1 ) ;
features . branchRailways = dedupePaths ( features . branchRailways || [ ] , 1 ) ;
debug . finalServiceAfterNearMissRepair = {
national : ensureMajorCityNationalRoadLinks ( 50000 ) ,
rail : ensureMajorCityRailLinks ( 50000 ) ,
expressway : ensureMajorCityExpresswayLinks ( 50000 ) ,
} ;
// Refill rural municipal access after the hard cleanup. This reuses the same
// terrain-aware local-road algorithm; no rectilinear/synthetic mesh is added.
const finalRuralCoverageStages = runFinalRuralMunicipalCoverageStages ( ) ;
debug . finalRuralMunicipalRoadCoverage = finalRuralCoverageStages . full ;
if ( finalRuralCoverageStages . visible ) debug . finalVisibleRuralMunicipalRoadCoverage = finalRuralCoverageStages . visible ;
const finalRuralDensificationStages = runFinalRuralDensificationStages ( ) ;
debug . finalRuralLocalDensification = finalRuralDensificationStages . full ;
if ( finalRuralDensificationStages . visible ) debug . finalVisibleRuralLocalDensification = finalRuralDensificationStages . visible ;
features . minorRoads = dedupePaths ( features . minorRoads || [ ] , 1 ) ;
debug . finalUrbanRailMultiplicity = ensureUrbanRailMultiplicity ( ) ;
debug . finalRailDensityProduction = densifyRailToNationalRatio ( initialVisibleCrop ? 1.00 : 0.98 , initialVisibleCrop || null ) ;
features . railways = dedupePaths ( features . railways || [ ] , 2 ) ;
features . branchRailways = dedupePaths ( features . branchRailways || [ ] , 2 ) ;
debug . finalRailDensityProductionCap = capRailDensityToNationalRatio ( 1.04 , initialVisibleCrop || null ) ;
const finalRailServiceAfterProductionCap = ensureMajorCityRailLinks ( 50000 ) ;
debug . finalRailServiceAfterProductionCap = finalRailServiceAfterProductionCap ;
// Density capping must not remove the sole *visible* inward rail approach of
// a crop-edge city. Re-run the exact visible-core service contract after the
// cap and never cap again after this point.
debug . finalVisibleServiceAfterRailCap = ensureVisibleCropMajorCityInternalService ( ) ;
features . railways = dedupePaths ( features . railways || [ ] , 2 ) ;
features . branchRailways = dedupePaths ( features . branchRailways || [ ] , 2 ) ;
debug . finalExpresswayTerminalContinuity = ensureExpresswayTerminalContinuity ( ) ;
features . expressways = dedupePaths ( features . expressways || [ ] , 2 ) ;
debug . finalExpresswayContinuityParallelPrune = pruneFinalParallelPaths ( features . expressways , "expressway" , finalMajorCities , { radius : 5 , threshold : 0.18 , directionDot : 0.92 , maxParallelRunSamples : 2 } ) ;
debug . finalExpresswayTerminalContinuityAfterParallelPrune = ensureExpresswayTerminalContinuity ( ) ;
features . expressways = dedupePaths ( features . expressways || [ ] , 2 ) ;
// Nothing after this point may be silently clipped by the renderer. Remove a
// whole route if it crosses forbidden water/mountain terrain, rebuild mandatory
// service with the strict terrain router, then turn near-misses into exact
// raster junctions. This prevents the old "route with the bad middle missing"
// appearance.
debug . finalTerrainInvalidTransportCleanupAfterAllDensity = removeTerrainInvalidTransportPaths ( ) ;
debug . finalServiceAfterStrictTerrainCleanup = {
national : ensureMajorCityNationalRoadLinks ( 50000 ) ,
rail : ensureMajorCityRailLinks ( 50000 ) ,
expressway : ensureMajorCityExpresswayLinks ( 50000 ) ,
} ;
const finalRailJunctionTarget = [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) , ... ( features . externalRailways || [ ] ) ] ;
debug . finalExactJunctionRepair = {
national : snapNearMissEndpoints ( features . nationalRoads || [ ] , [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] , "national" , { maxGap : 4.2 , maxAdded : 96 } ) ,
expressway : snapNearMissEndpoints ( features . expressways || [ ] , [ ... ( features . expressways || [ ] ) , ... ( features . externalExpressways || [ ] ) ] , "expressway" , { maxGap : 4.8 , maxAdded : 48 } ) ,
rail : snapNearMissEndpoints ( [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) ] , finalRailJunctionTarget , "rail" , { maxGap : 4.0 , maxAdded : 84 } ) ,
local : snapNearMissEndpoints ( features . minorRoads || [ ] , [ ... ( features . minorRoads || [ ] ) , ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] , "local" , { maxGap : 3.2 , maxAdded : 140 } ) ,
} ;
features . minorRoads = dedupePaths ( features . minorRoads || [ ] , 1 ) ;
features . nationalRoads = dedupePaths ( features . nationalRoads || [ ] , 1 ) ;
features . expressways = dedupePaths ( features . expressways || [ ] , 1 ) ;
features . railways = dedupePaths ( features . railways || [ ] , 1 ) ;
features . branchRailways = dedupePaths ( features . branchRailways || [ ] , 1 ) ;
debug . finalExpresswayTerminalContinuityAfterJunctionRepair = ensureExpresswayTerminalContinuity ( ) ;
features . expressways = dedupePaths ( features . expressways || [ ] , 1 ) ;
debug . finalTerrainInvariantAfterJunctionRepair = removeTerrainInvalidTransportPaths ( ) ;
debug . finalServiceAfterFinalTerrainInvariant = {
national : ensureMajorCityNationalRoadLinks ( 50000 ) ,
rail : ensureMajorCityRailLinks ( 50000 ) ,
expressway : ensureMajorCityExpresswayLinks ( 50000 ) ,
} ;
const afterInvariantRailTarget = [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) , ... ( features . externalRailways || [ ] ) ] ;
debug . finalExactJunctionRepairAfterInvariant = {
national : snapNearMissEndpoints ( features . nationalRoads || [ ] , [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] , "national" , { maxGap : 4.6 , maxAdded : 120 } ) ,
expressway : snapNearMissEndpoints ( features . expressways || [ ] , [ ... ( features . expressways || [ ] ) , ... ( features . externalExpressways || [ ] ) ] , "expressway" , { maxGap : 5.0 , maxAdded : 64 } ) ,
rail : snapNearMissEndpoints ( [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) ] , afterInvariantRailTarget , "rail" , { maxGap : 4.5 , maxAdded : 110 } ) ,
local : snapNearMissEndpoints ( features . minorRoads || [ ] , [ ... ( features . minorRoads || [ ] ) , ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] , "local" , { maxGap : 3.6 , maxAdded : 220 } ) ,
} ;
features . minorRoads = dedupePaths ( features . minorRoads || [ ] , 1 ) ;
features . nationalRoads = dedupePaths ( features . nationalRoads || [ ] , 1 ) ;
features . expressways = dedupePaths ( features . expressways || [ ] , 1 ) ;
features . railways = dedupePaths ( features . railways || [ ] , 1 ) ;
features . branchRailways = dedupePaths ( features . branchRailways || [ ] , 1 ) ;
debug . finalExpresswayTerminalContinuityAfterFinalTerrainInvariant = ensureExpresswayTerminalContinuity ( ) ;
features . expressways = dedupePaths ( features . expressways || [ ] , 1 ) ;
debug . finalInterchangeRebuild = rebuildFinalInterchanges ( ) ;
const missingMajorCityTrunkService = [ ] ;
const majorCityTrunkServiceExceptions = [ ] ;
const nationalNetwork = [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] ;
const railNetwork = [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) , ... ( features . externalRailways || [ ] ) ] ;
const expressNetwork = [ ... ( features . expressways || [ ] ) , ... ( features . externalExpressways || [ ] ) ] ;
for ( const city of finalMajorCities ) {
const national = anyPathTouches ( nationalNetwork , city , 3.0 ) ;
const rail = anyPathTouches ( railNetwork , city , 3.0 ) ;
const expressway = expresswayServesCityFringe ( city ) ;
if ( national && rail && expressway ) continue ;
const component = componentAt ( city ) ;
const componentArea = component >= 0 ? ( landComponentArea [ component ] || 0 ) : 0 ;
const alternateSameIslandTargets = sameComponentCivicTargets ( city , { minDistance : 8 , maxDistance : 240 , limit : 8 } ) ;
const absentOnComponent = {
national : ! national && component >= 0 && ! pathHasComponent ( nationalNetwork , component ) ,
rail : ! rail && component >= 0 && ! pathHasComponent ( railNetwork , component ) ,
expressway : ! expressway && component >= 0 && ! pathHasComponent ( expressNetwork , component ) ,
} ;
// The hidden overscan is context, not publishable content. A city that lies
// wholly outside the future published crop and close to the true hidden
// raster edge must not fail the visible production contract merely because
// its outward continuation is itself beyond the hidden planning halo.
const outsidePublishedCore = ! ! initialVisibleCrop
&& ! ( city . x >= initialVisibleCrop . x0 && city . y >= initialVisibleCrop . y0 && city . x < initialVisibleCrop . x1 && city . y < initialVisibleCrop . y1 ) ;
const hiddenOuterEdgeDistance = Math . min ( city . x , city . y , MAP _W - 1 - city . x , MAP _H - 1 - city . y ) ;
const hiddenOverscanEdgeOnly = ! ! initialVisibleCrop && outsidePublishedCore && hiddenOuterEdgeDistance < 36 ;
// A genuinely tiny island may reasonably have no motorway, but national and
// rail access on that island are still required. This matches the visible
// crop audit and prevents the old broad "no same-component network" escape
// hatch from excusing ordinary mainland cities.
const tinyIsletMotorwayException = component >= 0
&& componentArea < 96
&& national && rail && ! expressway ;
const genuineLargeStraitIsolation = tinyIsletMotorwayException
|| ( component >= 0 && componentArea < 72 && alternateSameIslandTargets . length === 0
&& ( absentOnComponent . national || absentOnComponent . rail || absentOnComponent . expressway ) ) ;
const unresolved = { national : ! national , rail : ! rail , expressway : ! expressway } ;
const record = { x : city . x , y : city . y , name : city . name , population : city . population || 0 , national , rail , expressway , landComponent : component , landComponentArea : componentArea } ;
if ( hiddenOverscanEdgeOnly ) {
majorCityTrunkServiceExceptions . push ( { ... record , exceptionReason : "hidden-overscan-edge-only" , hiddenOuterEdgeDistance , outsidePublishedCore : true , unresolved } ) ;
} else if ( genuineLargeStraitIsolation ) {
majorCityTrunkServiceExceptions . push ( { ... record , exceptionReason : tinyIsletMotorwayException ? "tiny-isolated-islet-motorway-not-required" : "tiny-isolated-islet-no-same-land-civic-anchor" , unavailableAcrossLargeStrait : absentOnComponent } ) ;
} else {
missingMajorCityTrunkService . push ( { ... record , unresolved } ) ;
}
}
debug . postDedupeMajorCityService = {
national : postDedupeNationalService ,
rail : postDedupeRailService ,
expressway : postDedupeExpressService ,
missing : missingMajorCityTrunkService ,
exceptions : majorCityTrunkServiceExceptions ,
} ;
2026-08-08 17:41:30 +09:00
if ( ! features . railways . length ) {
const railNodes = [ ... ( features . modernCities || [ ] ) , ... ( features . markets || [ ] ) ]
. filter ( ( p ) => p && inside ( p . x , p . y ) && ! terrain ? . sea ? . [ indexOf ( p . x , p . y ) ] )
. sort ( ( a , b ) => ( ( b . population || 0 ) + ( b . isPrefecturalCapital ? 800000 : 0 ) ) - ( ( a . population || 0 ) + ( a . isPrefecturalCapital ? 800000 : 0 ) ) ) ;
let fallbackRail = [ ] ;
for ( let a = 0 ; a < Math . min ( 8 , railNodes . length ) && ! fallbackRail . length ; a ++ ) {
const candidates = railNodes . slice ( a + 1 , Math . min ( 14 , railNodes . length ) )
. sort ( ( p , q ) => Math . hypot ( p . x - railNodes [ a ] . x , p . y - railNodes [ a ] . y ) - Math . hypot ( q . x - railNodes [ a ] . x , q . y - railNodes [ a ] . y ) ) ;
for ( const target of candidates ) {
const direct = Math . hypot ( target . x - railNodes [ a ] . x , target . y - railNodes [ a ] . y ) ;
if ( direct < 10 || direct > 150 ) continue ;
2026-08-11 21:51:07 +09:00
fallbackRail = routeTerrainPath ( railNodes [ a ] , target , terrain , { maxLength : direct * 3.4 + 82 , maxSeaRun : 0 , maxTunnelRun : 0 , snapRadius : 1.2 , maxElevation : 0.695 , strictTerrain : true } ) ;
if ( ! fallbackRail . length ) fallbackRail = terrainFirstConnector ( railNodes [ a ] , target , terrain , { skipPrimaryRoute : true , maxLength : direct * 3.6 + 88 , maxSeaRun : 0 , maxTunnelRun : 0 , snapRadius : 1.2 , maxElevation : 0.695 , strictTerrain : true } ) ;
2026-08-08 17:41:30 +09:00
if ( fallbackRail . length >= 4 ) break ;
fallbackRail = [ ] ;
}
}
if ( fallbackRail . length >= 4 ) {
2026-08-11 21:51:07 +09:00
fallbackRail = terrainSafeSmooth ( fallbackRail , terrain , "rail" , 1 ) ;
if ( hardTerrainPathValid ( fallbackRail , "rail" ) ) {
features . railways . push ( fallbackRail ) ;
debug . guaranteedRailFallbackAdded = 1 ;
} else {
debug . guaranteedRailFallbackAdded = 0 ;
}
2026-08-08 17:41:30 +09:00
} else {
debug . guaranteedRailFallbackAdded = 0 ;
}
}
2026-05-28 00:30:09 +09:00
features . interchanges = interchanges ;
2026-08-11 21:51:07 +09:00
// Final municipal access repair: connect an unserved office to the nearest
// existing road using the normal terrain-aware pathfinder. Do not create the
// former 2-3 cell isolated crossbar stub; those were the dominant source of
// meaningless rural dead-end roads.
2026-05-28 00:30:09 +09:00
let finalAdminStubsAdded = 0 ;
2026-08-11 21:51:07 +09:00
let finalAdminAccessUnresolved = 0 ;
2026-05-28 00:30:09 +09:00
for ( const center of adminCenters || [ ] ) {
if ( ! center || ! inside ( center . x , center . y ) ) continue ;
2026-08-11 21:51:07 +09:00
const network = [ ... features . minorRoads , ... features . nationalRoads , ... features . externalRoads ] ;
if ( anyPathTouches ( network , center , 0.85 ) ) continue ;
const target = nearestPointOnPaths ( network , center , 42 ) ;
if ( ! target ) { finalAdminAccessUnresolved ++ ; continue ; }
const d = Math . hypot ( target . x - center . x , target . y - center . y ) ;
if ( d < 2 ) continue ;
let path = routeTerrainPath ( center , target , terrain , { maxLength : d * 2.35 + 24 , maxSeaRun : 0 , maxTunnelRun : 6 , snapRadius : 1.2 } ) ;
if ( ! path . length ) path = terrainFirstConnector ( center , target , terrain , { skipPrimaryRoute : true , maxLength : d * 2.45 + 26 , maxSeaRun : 0 , maxTunnelRun : 6 , shortFallback : 6 } ) ;
if ( ! path . length || pathLengthCells ( path ) < 3 ) { finalAdminAccessUnresolved ++ ; continue ; }
features . minorRoads . push ( terrainSafeSmooth ( path , terrain , "local" , 1 ) ) ;
2026-05-28 00:30:09 +09:00
finalAdminStubsAdded ++ ;
}
debug . finalAdminStubsAdded = finalAdminStubsAdded ;
2026-08-11 21:51:07 +09:00
debug . finalAdminAccessUnresolved = finalAdminAccessUnresolved ;
debug . absoluteFinalTerrainInvariant = removeTerrainInvalidTransportPaths ( ) ;
debug . absoluteFinalMajorCityService = {
national : ensureMajorCityNationalRoadLinks ( 50000 ) ,
rail : ensureMajorCityRailLinks ( 50000 ) ,
expressway : ensureMajorCityExpresswayLinks ( 50000 ) ,
} ;
// Service guarantees are intentionally late, but they must not reintroduce
// kilometre-scale side-by-side trunks. Collapse required parallel corridors
// onto one existing terrain-valid physical alignment rather than deleting the
// route that uniquely serves a city. This preserves topology while removing
// the visual double-road failure.
debug . absoluteFinalNationalSharedAlignment = collapseParallelCorridorsOntoSharedAlignment (
features . nationalRoads , "national" , finalMajorCities , terrain ,
{ radius : 4 , directionDot : 0.90 , minRunPoints : 2 } ,
) ;
debug . absoluteFinalExpresswaySharedAlignment = collapseParallelCorridorsOntoSharedAlignment (
features . expressways , "expressway" , finalMajorCities , terrain ,
{ radius : 5 , directionDot : 0.91 , minRunPoints : 2 } ,
) ;
debug . absoluteFinalRuralNationalCoverage = ensureRuralNationalRoadCoverage ( ) ;
debug . absoluteFinalUrbanNationalGapRepair = repairUrbanNationalRoadGaps ( ) ;
debug . absoluteFinalNationalMajorCityServiceAfterRuralRepair = ensureMajorCityNationalRoadLinks ( 50000 ) ;
debug . absoluteFinalNationalSharedAlignmentAfterRuralCoverage = collapseParallelCorridorsOntoSharedAlignment (
features . nationalRoads , "national" , finalMajorCities , terrain ,
{ radius : 4 , directionDot : 0.90 , minRunPoints : 2 } ,
) ;
// Rural service and urban gap repair can legitimately create a short
// connector whose endpoints sit on an existing trunk. If most of that
// connector is already represented by the trunk, keep the physical shared
// alignment instead of a second side-by-side national-road object. Major-city
// unique service remains protected by the pruning helper.
debug . absoluteFinalNationalNearDuplicatePruneAfterRuralCoverage = pruneNearDuplicateTrunkPaths (
features . nationalRoads , "national" , finalMajorCities ,
{ overlapFloor : 0.44 , maxUniqueCells : 12 } ,
) ;
// Rural alignment promotion must not reintroduce the old several-kilometre
// side-by-side national-road artifact. Remove only redundant parallel trunks;
// exact shared alignments remain valid multiplexed corridors.
debug . absoluteFinalNationalParallelPruneAfterRuralCoverage = pruneFinalParallelPaths (
features . nationalRoads , "national" , [ ] ,
{ radius : 3 , threshold : 0.38 , directionDot : 0.90 , maxParallelRunSamples : 5 } ,
) ;
// This late anti-parallel pass intentionally ignores route-level major-city
// ownership. Rebuild any service it removed as a short terrain-routed access
// connector instead of preserving kilometres of duplicate trunk geometry.
debug . absoluteFinalNationalMajorCityServiceAfterParallelPrune = ensureMajorCityNationalRoadLinks ( 50000 ) ;
features . nationalRoads = dedupePaths ( features . nationalRoads || [ ] , 1 ) ;
debug . absoluteFinalRailDensity = densifyRailToNationalRatio ( initialVisibleCrop ? 0.94 : 0.92 , initialVisibleCrop || null ) ;
debug . absoluteFinalRailDensityCap = capRailDensityToNationalRatio ( 0.96 , initialVisibleCrop || null ) ;
debug . absoluteFinalRailMajorCityService = ensureMajorCityRailLinks ( 50000 ) ;
// Rail densification can reveal the only terrain-valid valley corridor for a
// large-city motorway approach. Re-run the motorway service guarantee after
// the *complete* trunk network exists, then normalize any newly shared
// motorway geometry before the final IC reconstruction.
debug . absoluteFinalExpresswayMajorCityServiceAfterRailDensity = ensureMajorCityExpresswayLinks ( 50000 ) ;
debug . absoluteFinalExpresswaySharedAlignmentAfterRailDensity = collapseParallelCorridorsOntoSharedAlignment (
features . expressways , "expressway" , finalMajorCities , terrain ,
{ radius : 5 , directionDot : 0.91 , minRunPoints : 2 } ,
) ;
debug . absoluteFinalExpresswayNearDuplicatePruneAfterRailDensity = pruneNearDuplicateTrunkPaths (
features . expressways , "expressway" , finalMajorCities ,
{ overlapFloor : 0.44 , maxUniqueCells : 12 } ,
) ;
function pruneAbsoluteShortExpresswayFragments ( ) {
const current = features . expressways || [ ] ;
const external = features . externalExpressways || [ ] ;
const result = { before : current . length , removed : 0 , after : current . length } ;
const keep = [ ] ;
for ( let index = 0 ; index < current . length ; index ++ ) {
const path = current [ index ] ;
const len = pathLengthCells ( path || [ ] ) ;
if ( ! path ? . length ) { result . removed ++ ; continue ; }
const a = path [ 0 ] , b = path [ path . length - 1 ] ;
const touchesEdge = a [ 0 ] <= 2 || a [ 1 ] <= 2 || a [ 0 ] >= MAP _W - 3 || a [ 1 ] >= MAP _H - 3
|| b [ 0 ] <= 2 || b [ 1 ] <= 2 || b [ 0 ] >= MAP _W - 3 || b [ 1 ] >= MAP _H - 3 ;
if ( len >= 10 || touchesEdge || finalMajorCities . some ( ( city ) => pathServesMajorCity ( path , city , "expressway" ) ) ) { keep . push ( path ) ; continue ; }
const others = [ ... current . filter ( ( _ , j ) => j !== index ) , ... external ] ;
const aJoin = nearestPointOnPaths ( others , { x : a [ 0 ] , y : a [ 1 ] } , 3.0 ) ;
const bJoin = nearestPointOnPaths ( others , { x : b [ 0 ] , y : b [ 1 ] } , 3.0 ) ;
if ( aJoin && bJoin ) keep . push ( path ) ; else result . removed ++ ;
}
features . expressways = keep ;
result . after = keep . length ;
return result ;
}
debug . absoluteFinalShortExpresswayCleanup = pruneAbsoluteShortExpresswayFragments ( ) ;
const absoluteRailTargets = [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) , ... ( features . externalRailways || [ ] ) ] ;
debug . absoluteFinalNearMissJunctionRepair = {
national : snapNearMissEndpoints ( features . nationalRoads || [ ] , [ ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] , "national" , { maxGap : 4.6 , maxAdded : 120 } ) ,
expressway : snapNearMissEndpoints ( features . expressways || [ ] , [ ... ( features . expressways || [ ] ) , ... ( features . externalExpressways || [ ] ) ] , "expressway" , { maxGap : 5.0 , maxAdded : 64 } ) ,
rail : snapNearMissEndpoints ( [ ... ( features . railways || [ ] ) , ... ( features . branchRailways || [ ] ) ] , absoluteRailTargets , "rail" , { maxGap : 4.5 , maxAdded : 110 } ) ,
local : snapNearMissEndpoints ( features . minorRoads || [ ] , [ ... ( features . minorRoads || [ ] ) , ... ( features . nationalRoads || [ ] ) , ... ( features . externalRoads || [ ] ) ] , "local" , { maxGap : 3.6 , maxAdded : 220 } ) ,
} ;
debug . absoluteFinalExpresswayTerminalContinuity = ensureExpresswayTerminalContinuity ( ) ;
features . minorRoads = dedupePaths ( features . minorRoads || [ ] , 1 ) ;
features . nationalRoads = dedupePaths ( features . nationalRoads || [ ] , 1 ) ;
features . expressways = dedupePaths ( features . expressways || [ ] , 1 ) ;
features . railways = dedupePaths ( features . railways || [ ] , 1 ) ;
features . branchRailways = dedupePaths ( features . branchRailways || [ ] , 1 ) ;
debug . absoluteFinalInterchangeRebuild = rebuildFinalInterchanges ( ) ;
2026-05-28 00:30:09 +09:00
features . roadInfluence = rebuildInfluence ( [ ... features . nationalRoads , ... ( features . ringRoads || [ ] ) , ... features . externalRoads , ... features . minorRoads ] , 5.0 ) ;
features . roadDensityInfluence = rebuildInfluence ( [ ... features . nationalRoads , ... ( features . ringRoads || [ ] ) , ... features . externalRoads , ... features . minorRoads ] , 9.0 ) ;
features . transportDebug = {
... ( features . transportDebug || { } ) ,
generationOrder : debug . order ,
postAdminTransportFinalization : debug ,
} ;
return features ;
}