Offline and mobile
Download offline maps
Choose prebuilt home areas or build a mission pack, then download and verify every file.
On this page
Find overview and home-area packs #
Prerequisites: maps scope, storage space and a connection to the maps host while downloading. Pack generation also needs packs scope. Discover packs from the current release instead of constructing IDs or assuming a fixed national pack count.
import { configure, releaseInfo, mapsUrl } from 'https://maps.example/sw-services.js';
configure({ baseUrl: 'https://maps.example/', apiKey: 'YOUR_API_KEY' });
const headers = { 'X-API-Key': 'YOUR_API_KEY' };
const release = await releaseInfo();
const overview = release.packs.find(p => p.kind === 'overview');
const station = [123.9, 10.3];
const home = release.packs.find(p => p.kind !== 'overview' && p.bbox &&
station[0] >= p.bbox[0] && station[0] < p.bbox[2] &&
station[1] >= p.bbox[1] && station[1] < p.bbox[3]);
const selected = home ?? overview;
if (!selected) throw new Error('No suitable prebuilt pack');
const manifestUrl = mapsUrl(selected.manifest.url);
const response = await fetch(manifestUrl, { headers });
if (!response.ok) throw new Error('Pack manifest unavailable');
const manifest = await response.json();
const downloads = manifest.files.map(file => ({ ...file,
downloadUrl: new URL(file.path, manifestUrl).href }));
console.log(manifest.packId, manifest.totalBytes, downloads);Expected result: a manifest and the absolute URL of every file. The overview is a small national map (base z0–8, bathymetry z3–6). Home areas are coastal 1° cells with base z0–14 and selected bathymetry/contours. A station outside listed coastal cells may need a custom mission pack. Sizes and counts vary by release. Prebuilt file paths resolve relative to their manifest URL, not the portal URL.
Estimate and build a mission pack #
import { configure, createPack, mapsUrl } from 'https://maps.example/sw-services.js';
configure({ baseUrl: 'https://maps.example/', apiKey: 'YOUR_API_KEY' });
const request = {
release: 'current', bbox: [123.6, 10.1, 124.1, 10.6],
shore: [[123.88, 10.28, 123.95, 10.35]],
layers: ['base', 'gmrt', 'contours'], styles: ['dark', 'light']
};
const estimate = await createPack({ ...request, dryRun: true });
console.log(estimate.totalBytes, estimate.files);
// Show the estimate, get the user's download choice, then build:
const pack = await createPack(request);
const downloads = pack.files.map(file => ({ ...file,
downloadUrl: mapsUrl(file.url) }));
console.log(pack.packId, downloads);Check the release layers before selecting GMRT or another optional dataset. base is always included; requesting GMRT also includes GEBCO beneath it. HTTP 201 means newly built; 200 means cached (dry runs also return 200). A dry run does not start the download. Tile estimates are exact, but fonts.tar may be estimated; use the final manifest sizes for integrity checks and final approval if size changed.
Mission limits and caching #
| Rule | Default / error |
|---|---|
| Main box | Up to 90 × 90 NM at its center latitude; box-too-large |
| Shore boxes | At most 8; each up to 10 × 10 NM. Send inside the main box; non-overlapping boxes are rejected and overlapping boxes are clipped after rounding. Errors: too-many-shore-boxes, shore-box-too-large, shore-outside-box |
| Pack size | At most 75,000,000 bytes; pack-too-large |
| Coverage | Box must touch release coverage; outside-coverage |
| Base zooms | z0–14 throughout; z15 only inside shore boxes |
| Request rounding | Boxes rounded outward to 0.05°. Use the manifest bbox and shore for actual extents. |
| Build rate | Default 30 new packs/hour per authenticated client plus optional gateway user; 429 rate-limited. Cached requests do not spend the budget. |
| Invalid request | 400 bad-request for malformed JSON/unknown choices; 422 for limit refusals |
Builds are queued and matching requests share work. Use the returned packId to retrieve a manifest with GET /maps/v1/packs/<packId>. Server caches can evict packs or retire them when their release disappears. A saved ID is not permanent remote storage: retain the downloaded files and manifest on the device.
Download, resume and verify #
A pack is a directory of files, not one ZIP download. Authenticate each HTTP request, resume supported downloads with Range, then check size and SHA-256 against the manifest. A complete local pack remains usable after the issuing key expires; that key is needed again for protected server requests.
// Browser helper; target is a FileSystemFileHandle in a staging folder.
async function downloadVerified(file, url, target, apiKey) {
const existing = await target.getFile();
let start = existing.size;
if (start > file.bytes) start = 0;
if (start < file.bytes) {
const headers = { 'X-API-Key': apiKey };
if (start) headers.Range = 'bytes=' + start + '-';
const res = await fetch(url, { headers });
if (!res.ok) throw new Error('Download HTTP ' + res.status);
if (res.status === 206) {
const expected = 'bytes ' + start + '-';
if (!res.headers.get('Content-Range')?.startsWith(expected))
throw new Error('Unexpected resume offset');
} else start = 0; // Server sent a complete 200: replace, do not append.
const writer = await target.createWritable({ keepExistingData: Boolean(start) });
const reader = res.body.getReader();
try {
await writer.seek(start);
for (;;) {
const { done, value } = await reader.read();
if (done) break;
await writer.write(value);
}
} finally {
await writer.close(); // Preserve any partial bytes for a later retry.
reader.releaseLock();
}
}
const complete = await target.getFile();
const digest = await crypto.subtle.digest('SHA-256', await complete.arrayBuffer());
const hex = [...new Uint8Array(digest)].map(x => x.toString(16).padStart(2, '0')).join('');
if (complete.size !== file.bytes || hex !== file.sha256) {
const reset = await target.createWritable(); await reset.close();
throw new Error('Integrity check failed; retry from zero');
}
}Call the helper for each manifest entry with its resolved URL and staging file handle. The example hashes a whole file in memory; use incremental hashing in a worker if device memory is constrained. If a response is interrupted, retry against the same immutable file/manifest. Only expose the pack as ready after every file verifies and the fonts are extracted. A SHA-256 match checks integrity against the manifest; it does not independently authenticate a manifest obtained from an untrusted source.