fix(tools): ship types read as their family; letterless destinations are unknown

AIS sends ship types as ITU-R M.1371 codes, so vessels_in_area grouped
"70", "74" and "79" as three types, and type: "tanker" matched nothing on
feeds that send codes. Codes now read as the family the vessel cards already
show (CARGO, TANKER, TUG), for rows, group_by and the type filter. The code
table moves to src/data/vesselTypes.js, which has no i18n import, so the
server-side tools can use it; vesselLabels re-exports it.

A destination with no letters ("0", "---") now counts as unknown.

Live, Singapore: "most by type: CARGO (3), TANKER (3), PASSENGER (1)"; "how
many tankers near Singapore" found 3.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
quanticsoul4772andClaude Opus 5.5 committed 2026-10-17 15:49:02 -07:00
1 parent d06e485c12
commit d39256e700
6 files changed
+126 -48

No files matched your search

+15 -7
View File
@@ -20,6 +20,7 @@ import {
numericSummary,
thinEvenly,
} from '../results.js';
import { normalizeVesselType } from '../../data/vesselTypes.js';
const MMSI_SCHEMA = Object.freeze({
type: 'string',
@@ -32,11 +33,17 @@ const ALL_VESSELS = 1_000_000;
const round = (value, digits) =>
Number.isFinite(value) ? Number(value.toFixed(digits)) : null;
const text = (value) => (typeof value === 'string' && value ? value : null);
// AIS ship type 0 is "not available" (ITU-R M.1371); the source keeps it as
// '0', so it is reported as unknown rather than counted as a type.
const vesselType = (value) => {
const type = text(value);
return type === '0' ? null : type;
// AIS sends ship types as ITU-R M.1371 codes ("70" to "79" are all cargo), so
// a code reads as its family ("CARGO"), which is what group_by counts and the
// type filter matches; text types pass through. Type 0 is "not available" and
// reads as unknown.
const vesselType = (value) => normalizeVesselType(text(value) ?? '') || null;
// Crews type the destination by hand; one with no letters ("0", "---") names
// no port and reads as unknown.
const vesselDestination = (value) => {
const destination = text(value);
return destination && /\p{L}/u.test(destination) ? destination : null;
};
function vesselRow(record, from) {
@@ -46,7 +53,7 @@ function vesselRow(record, from) {
name: text(record.name),
imo: text(record.imo),
type: vesselType(record.type),
destination: text(record.destination),
destination: vesselDestination(record.destination),
lat: round(record.latitude, 5),
lon: round(record.longitude, 5),
speed_mps: round(record.speedMps, 1),
@@ -134,7 +141,8 @@ export const vesselsInArea = defineTool({
.filter(
(record) =>
areaContains(area, { lat: record.latitude, lon: record.longitude }) &&
(!wanted || String(record.type).toLowerCase().includes(wanted)),
(!wanted ||
(vesselType(record.type) ?? '').toLowerCase().includes(wanted)),
)
.map((record) => vesselRow(record, center))
.sort((a, b) => a.distance_km - b.distance_km);