Restrict routing to maritime ports
Build and deploy Docker image / build (push) Successful in 27s

This commit is contained in:
Ruslan Bakiev
2026-09-29 19:48:41 +07:00
parent 77202f1f8d
commit 04e275da93
4 changed files with 130 additions and 4 deletions
+2
View File
@@ -1,6 +1,7 @@
import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'
import { ensureGraph, getDb } from '../dist/db.js'
import { AFRICA_MARITIME_LOCODES } from '../dist/maritime.js'
type SupplierSeed = { lei: string; name: string; countryCode: string; city: string }
type HubSeed = {
@@ -155,6 +156,7 @@ const hubNodes = hubs.map((hub) => {
country,
country_code: hub.countryCode,
transport_types: transportTypes(hub.functionCode),
is_maritime_port: AFRICA_MARITIME_LOCODES.has(hub.code),
navigation_priority: priorityHubCodes.has(hub.code) ? 1 : 0,
source: 'UN/LOCODE Africa',
locode: hub.code,
+18
View File
@@ -0,0 +1,18 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { isMaritimePort } from './maritime.js'
test('accepts curated coastal LOCODE ports', () => {
assert.equal(isMaritimePort({ locode: 'NGLAG' }), true)
assert.equal(isMaritimePort({ locode: 'CDBNW' }), true)
})
test('rejects inland ports even when UN/LOCODE function 1 is present', () => {
assert.equal(isMaritimePort({ locode: 'CMGOU', function_code: '1-34----' }), false)
assert.equal(isMaritimePort({ locode: 'CGOYO', function_code: '1-3-----' }), false)
})
test('accepts an explicitly curated database record', () => {
assert.equal(isMaritimePort({ locode: 'CUSTOM', is_maritime_port: true }), true)
})
+102
View File
@@ -0,0 +1,102 @@
import type { ArangoDoc } from './helpers.js'
// UN/LOCODE function 1 also includes inland waterway ports. This curated set
// keeps the maritime planner on coastal and ocean-accessible terminals only.
export const AFRICA_MARITIME_LOCODES = new Set([
'BJPTN',
'CDBNW',
'CIASS',
'CIBAO',
'CIDAB',
'CIFRE',
'CIGLU',
'CISPY',
'CMLIM',
'CMMOU',
'CVMIN',
'DJDMJ',
'DJPOD',
'DJDCT',
'DZALG',
'DZAZW',
'DZCHE',
'DZCOL',
'DZDEL',
'DZGHZ',
'EGAGN',
'EGADA',
'EGAQU',
'EGSUZ',
'EGDOM',
'EGDAM',
'EGGUA',
'EGGAL',
'EGIKU',
'EGEPP',
'EGOPT',
'EGGLB',
'EGPSE',
'EGMEA',
'EGSBY',
'ERASA',
'ERMSW',
'GNBEL',
'GNBFA',
'GNDPL',
'GNPMT',
'GNDKA',
'GNKMC',
'GWOXB',
'GWBOL',
'GWBQE',
'GWCAC',
'KESMN',
'KMAJN',
'LRHAR',
'LRMLW',
'LYKHO',
'LYELK',
'LYMEL',
'MAANZ',
'MABUJ',
'MAELJ',
'MAESS',
'MARNA',
'MGNOS',
'MUPMA',
'MZCOR',
'MZMZB',
'NGFLM',
'NGLAG',
'NGLKK',
'NGONN',
'SCVIC',
'SLKSY',
'SOELM',
'SOGRC',
'STTMS',
'TNHMM',
'TNTZE',
'TNBJC',
'TNCHB',
'TNMAD',
'TNRDS',
'TNSLN',
'TNZRZ',
'TZDAR',
'TZIKW',
'ZACPT',
'ZADUR',
'ZAKOM',
'ZAPNL',
'ZAPED',
'ZAJOH',
'ZAZE4',
'ZASMN',
'ZASTD',
])
export function isMaritimePort(document: ArangoDoc): boolean {
return document.is_maritime_port === true
|| AFRICA_MARITIME_LOCODES.has(String(document.locode ?? '').toUpperCase())
}
+8 -4
View File
@@ -378,18 +378,18 @@ async function maritimeRoutesBetweenNodes(
FOR port IN nodes
FILTER port.node_type == 'logistics' OR port.node_type == null
FILTER port.latitude != null AND port.longitude != null
FILTER 'sea' IN port.transport_types
FILTER port.is_maritime_port == true
SORT DISTANCE(@source_lat, @source_lon, port.latitude, port.longitude) ASC
LIMIT 8
LIMIT 12
RETURN port
)
LET destination_ports = (
FOR port IN nodes
FILTER port.node_type == 'logistics' OR port.node_type == null
FILTER port.latitude != null AND port.longitude != null
FILTER 'sea' IN port.transport_types
FILTER port.is_maritime_port == true
SORT DISTANCE(@destination_lat, @destination_lon, port.latitude, port.longitude) ASC
LIMIT 8
LIMIT 12
RETURN port
)
FOR origin_port IN origin_ports
@@ -398,6 +398,10 @@ async function maritimeRoutesBetweenNodes(
LET first_mile = DISTANCE(@source_lat, @source_lon, origin_port.latitude, origin_port.longitude)
LET sea_leg = DISTANCE(origin_port.latitude, origin_port.longitude, destination_port.latitude, destination_port.longitude)
LET last_mile = DISTANCE(destination_port.latitude, destination_port.longitude, @destination_lat, @destination_lon)
LET origin_to_destination = DISTANCE(@destination_lat, @destination_lon, origin_port.latitude, origin_port.longitude)
LET destination_to_source = DISTANCE(@source_lat, @source_lon, destination_port.latitude, destination_port.longitude)
FILTER first_mile < origin_to_destination
FILTER last_mile < destination_to_source
SORT first_mile + sea_leg + last_mile ASC
LIMIT @limit
RETURN { origin_port, destination_port }