Cebu journey endpoint-role evidence — immutable supplement v1 This is an evidence subset, not a passenger instruction or permission. Pattern stop IDs are looked up exactly; relation identities come only from the pattern's declared geometry source. Literal node-member roles and zero-based relation member indices retain source order. Points not directly present as node members retain only actual native member-way endpoint identities; no platform role or boarding approval is inferred. No coordinates are included or changed. Inputs: download ../2026-10-07-v2/osm-derived-database.json. Its network and sourceNativeData.citySnapshot are complete inputs; sourceSnapshots supplies the recorded City requested/server/capture clocks and retained wrapper checksum. The local builder independently deep-compares these inputs with the immutable app network and raw wrapper before writing. The result below is the entire shipped ledger; computed presentation is not routing data. Complete deterministic extraction method (Node.js, standard library only): Save the following as derive.mjs, then run node derive.mjs osm-derived-database.json > reproduced-endpoint-roles.json. The result must byte-equal endpoint-roles.json. import fs from 'node:fs'; function deriveEndpointRoles(network, snapshot, sourceSnapshot) { const objects = new Map(snapshot.elements.map(object => [`${object.type}/${object.id}`, object])); const points = new Map(); const patterns = network.patterns.filter(pattern => pattern.previewRoutingEligible).map(pattern => { const source = network.sources.find(record => record.id === pattern.geometrySourceId); const relationId = Number(source?.url.match(/^https:\/\/www\.openstreetmap\.org\/relation\/(\d+)$/)?.[1]); const relation = objects.get(`relation/${relationId}`); if (!relation || !relation.version || !relation.timestamp) throw Error(`Missing exact relation for ${pattern.id}`); const nodeIds = new Set(pattern.stopIds.map(stopId => { const nodeId = Number(stopId.match(/^osm-node-(\d+)$/)?.[1]); const native = objects.get(`node/${nodeId}`); const point = network.stops.find(record => record.id === stopId); if (!native || !native.version || !native.timestamp || !point || point.lat !== native.lat || point.lon !== native.lon) throw Error(`Missing/mismatched native point ${stopId}`); points.set(stopId, { stopId, nodeId, nodeVersion: native.version, nodeEditedAt: native.timestamp }); return nodeId; })); const nodeMembers = relation.members.flatMap((member, memberIndex) => member.type === 'node' && nodeIds.has(member.ref) ? [{ nodeId: member.ref, memberIndex, role: member.role }] : []); const directIds = new Set(nodeMembers.map(member => member.nodeId)); const wayEndpoints = relation.members.flatMap((member, memberIndex) => { if (member.type !== 'way') return []; const way = objects.get(`way/${member.ref}`); if (!way?.nodes?.length) return []; return [['first', way.nodes[0]], ['last', way.nodes.at(-1)]].flatMap(([endpoint, nodeId]) => nodeIds.has(nodeId) && !directIds.has(nodeId) ? [{ nodeId, wayId: way.id, wayVersion: way.version, wayEditedAt: way.timestamp, memberIndex, memberRole: member.role, endpoint }] : []); }); return { patternId: pattern.id, sourceId: source.id, relationId, relationVersion: relation.version, relationEditedAt: relation.timestamp, nodeMembers, wayEndpoints }; }); return { schemaVersion: 1, packageId: 'cebu-journey-endpoint-roles-2026-10-07-v1', attribution: 'OpenStreetMap contributors', copyrightURI: 'https://www.openstreetmap.org/copyright', licenseURI: 'https://opendatacommons.org/licenses/odbl/1-0/', sourceSnapshot, patterns, points: [...points.values()] }; } const offer = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')); const source = offer.sourceSnapshots.find(record => record.id === 'city-route-and-parent-extract'); console.log(JSON.stringify(deriveEndpointRoles(offer.network, offer.sourceNativeData.citySnapshot, source), null, 2)); Attribution/licence: see NOTICE.txt. Source clocks are map/database/capture times, not current service validity. Public permission, gates, approach connections, crossings, accessibility, fare and departures remain unverified. A01 GTFS, artwork, tiles, passenger-policy documents and user locations are excluded.