# Loading the Region Catalog

This page explains how to load and refresh the route-network and map-tile region catalogs before users choose required offline regions. Use cached data first, synchronize catalogs when needed, and handle partial catalog results from each subsystem.

## Cache and TTL

_syncRegions()_ synchronizes the road-network and map catalogs in parallel and applies the same cache policy to both. The default TTL is 24 hours. Each catalog atomically replaces its own cache, and a failed refresh does not delete the last successful result.

| Mode | Network behavior | Recommended use |
| --- | --- | --- |
| `AUTO` | Skips the network when the cache is complete and within its TTL. It refreshes only when missing or expired | Screen entry and automatic background refresh |
| `FORCE` | Always refreshes from the network and returns an error when no validated network is available | User-initiated pull to refresh |

_fetchRegionLists()_ reads only the local road-network and map caches without making a network request. To avoid leaving the region screen in a loading state, read and display the cache first, then run `AUTO` and read the caches again after synchronization. Keep the initial loading state only for a first installation with no cached catalog.

## Cache-first Screen Implementation

```kotlin
private var catalogReadToken = 0L
fun loadRegionCatalog(force: Boolean) {
    readCachedRegions()
    val mode = if (force) RegionListSyncMode.FORCE else RegionListSyncMode.AUTO
    NBNavigation.syncRegions(
        mode,
        object : NBNavigationTaskCallback<OfflineRegionCatalogSyncResult> {
            override fun onSuccess(result: OfflineRegionCatalogSyncResult?) {
                readCachedRegions()
            }
            override fun onError(throwable: Throwable) {
                readCachedRegions()
                showCatalogRefreshWarning(throwable)
            }
        }
    )
}
private fun readCachedRegions() {
    val token = ++catalogReadToken
    NBNavigation.fetchRegionLists(
        "United States",
        object : NBNavigationTaskCallback<OfflineRegionListResult> {
            override fun onSuccess(result: OfflineRegionListResult?) {
                if (token != catalogReadToken || result == null) return
                showRegions(result.regionRows.filter { it.hasRoute && it.hasMap })
                result.routeError?.let(::showRouteCatalogWarning)
                result.mapError?.let(::showMapCatalogWarning)
            }
            override fun onError(throwable: Throwable) {
                if (token == catalogReadToken) {
                    showOfflineError("Loading cached regions failed", throwable)
                }
            }
        }
    )
}
```

_OfflineRegionCatalogSyncResult_ contains synchronization details for both route and map catalogs. `map.networkSkipped` is true when `AUTO` uses a valid cached map catalog. A successful sync does not guarantee that downloadable regions exist. To verify, call _fetchRegionLists()_ after synchronization to read the catalogs.

## Country Filtering and Partial Results

The no-argument _fetchRegionLists()_ overload queries **United States** by default. `NBNavigation` does not currently expose a public country-enumeration API. Obtain the supported full English country name from the deployment configuration, your product backend, or NextBillion.ai, then pass that exact value to _fetchRegionLists(country, callback)_. Do not pass an ISO code, a localized name, or a whitespace-only string. Treat the country as product configuration rather than deriving it from the device locale.

_OfflineRegionListResult_ may contain data from only the road-network or map side. Check `routeError`, `mapError`, `hasRouteData`, `hasMapData`, and `regionRows` together. Do not interpret an empty list after filtering on `hasRoute` and `hasMap` as proof that no data exists, because the screen can display whichever cache remains available.

Reading installed map regions on a cold start may access the disk. Use the asynchronous _listInstalledMapTileRegions()_ API instead of reading Maps SDK storage directly on the main thread.

### Region List Fields, Data Size, and Update Detection

After _fetchRegionLists()_ succeeds, use `result.regionRows` as the primary screen model. It aligns route and map catalogs by `regionId` and preserves regions available from only one source. Filter on `hasRoute && hasMap` only when the product explicitly requires a complete offline-navigation package.

### Result object OfflineRegionListResult

| Field | Meaning | Recommended use |
| --- | --- | --- |
| `routeRegionList` | Locally cached route catalog; null when unavailable or unreadable | Use for catalog metadata or grouped route queries |
| `mapRegionList` | Locally cached map catalog; null when unavailable or unreadable | Use for catalog metadata or map diagnostics |
| `routeError` / `mapError` | Source-specific error or stale-cache warning | The other source and regionRows may still be usable |
| `regionRows` | Route and map entries aligned by _regionId_ | Preferred source for a regional-list UI |
| `updatedAtMs` | Wall-clock time when this combined result was constructed | This is not the last catalog-sync time |
| `hasRouteData` | Whether _routeRegionList_ exists | Does not mean every row contains route data |
| `hasMapData` | Whether _mapRegionList_ exists | Does not mean every row contains map data |

Read _routeRegionList.listSyncedAtMs_ and _mapRegionList.listSyncedAtMs_ for catalog freshness; `updatedAtMs` only timestamps this response. _mapRegionList.listSyncStatus_ can be `empty`, `ok`, or `failed`. Do not infer synchronization success from total or an empty list alone without simultaneously inspecting `routeError` and `mapError`.

### List item OfflineRegionListRow

| Field | Meaning | Recommended use |
| --- | --- | --- |
| `regionId` | Stable ID shared by the route and map catalogs | Use for download, pause, resume, cancel, delete, and version association |
| `routeRegion` | _OfflineRegionLeaf_ for this ID. It will be “null” when absent from the route catalog | Read route version, download state, progress, and bounds |
| `mapRegion` | _MapTileRegionListItem_ for this ID. It will be “null” when absent from the map catalog | Read map version, estimated size, and tile count |
| `displayName` | Route `adminL3`, then map name, then Region #id | Suitable as the default UI title |
| `country` | Route country, then map `adminL0` | Can be “null”. Do not query with a localized replacement |
| `adminL1` | Route `adminL1`, then map `adminL1` | May be “null” |
| `adminL2` | Route-catalog level-2 administrative name | The map catalog does not currently supply it. Can be “null” |
| `adminL3` | Route `adminL3`, then map `adminL3` | Can be “null” |
| `routeSizeBytes` | Estimated compressed route download size in bytes | “null” means the route side is absent but it does not mean zero size. |
| `mapSizeBytes` | Estimated compressed map download size in bytes | “null” means the map side is absent but it does not mean zero size. |
| `hasRoute` | _routeRegion != null_ | Whether the row can provide a route package |
| `hasMap` | _mapRegion != null_ | Whether the row can provide a map package |

### Route item routeRegion and OfflineRegionLeaf

| Field | Meaning | Recommended use |
| --- | --- | --- |
| `regionId` | Route-region ID | Should equal the outer row.regionId |
| `country / adminL1 / adminL2 / adminL3` | Catalog administrative hierarchy | adminL3 is normally displayed; values can be empty strings |
| `version` | Current server route-catalog version | Normally consumed by the SDK updateAvailable calculation |
| `timestampSec` | Catalog-version time in Unix epoch seconds | Used for version comparison; dataVersionLabel is available for display |
| `totalSizeBytes` | Estimated compressed route-package size | Use for pre-download display and storage estimates |
| `downloadStatus` | Can be one of _IDLE_, _DOWNLOADING_, _COMPLETE_, _PARTIAL_, _FAILED_, _PAUSED_, or _UNKNOWN_ | Prefer _isDownloaded_, _isPartial_, or unified progress for business decisions |
| `tilesTotal` / `tilesDone` | Planned and completed route-tile counts | Calculate detailed progress only when the total is valid |
| `detailSyncStatus` | Raw region-detail or manifest synchronization state | Use for diagnostics, not as the only readiness gate |
| `catalogRemoved` | The local entry is absent from the latest server catalog | Local data remains, so do not treat it as an update |
| `catalogUpdateAvailable` | SDK-computed route-catalog update flag | Prefer the equivalent _updateAvailable_ property |
| `boundaryBbox` | [WGS84](https://en.wikipedia.org/wiki/World_Geodetic_System) bounds derived from cached boundary GeoJSON | Use for viewport intersection when detail is present |
| `catalogBbox` | [WGS84](https://en.wikipedia.org/wiki/World_Geodetic_System) bounds supplied directly by the catalog | Use for coarse filtering before boundary detail is downloaded |
| `displayName` | Derived from _adminL3_, _adminL2_, or _regionId_ | The outer `row.displayName` also considers the map name |
| sizeWithUnit\` | Human-readable _totalSizeBytes_ | Useful when displaying only the route-package size |
| `isDownloaded` / \`isPartial | Derived from status and _tilesDone_/ _tilesTotal_ | Use for route-side availability and resume controls |
| `updateAvailable` | Public alias of _catalogUpdateAvailable_ | After catalog sync, use it to show a route-update prompt |
| `dataVersionLabel` | _timestampSec_ formatted as yyyy-MM-dd | Display only. Do not parse it back for version comparison |

### Map item mapRegion and MapTileRegionListItem

| Field | Meaning | Recommended use |
| --- | --- | --- |
| `regionId` | Map-region ID | Should equal the outer `row.regionId` |
| `name` | Map-region name from the server catalog | Used by `row.displayName` when the route name is absent |
| `adminL0` | English country or level-0 administrative name | Used when filtering the map catalog by country |
| `adminL1` | Level-1 administrative name | Can be used for grouped display |
| `adminL3` | Level-3 administrative name | Can be used for finer display. _adminL2_ is currently not supplied |
| `version` | Server map-region version. It can be null | Compare with the installed `regionVersion` |
| `totalSizeBytes` | Estimated compressed map-package size | Use for pre-download display and storage estimates |
| `tileCount` | Server-provided estimated tile count | An estimate, not a package-count limit |
| `timestamp` | Raw server timestamp. It can be null and is preserved without unit conversion | Do not assume it to be in seconds. Prefer `version` for map-update comparison |

## Calculate download size and available storage

For a first download, add non-null `routeSizeBytes` and `mapSizeBytes` to obtain the combined estimated compressed download size. This excludes extraction, database pages, temporary files, and filesystem overhead, so it is not the final on-disk size.

For a resumed download, when downloaded and total are available from _observeOfflineProgress()_, sum _max(total - downloaded, 0)_ for both subtasks or else fall back to the full catalog estimate.

```kotlin
private fun estimatedBytes(row: OfflineRegionListRow): Long {
    val route = (row.routeSizeBytes ?: 0L).coerceAtLeast(0L)
    val map = (row.mapSizeBytes ?: 0L).coerceAtLeast(0L)
    return if (route > Long.MAX_VALUE - map) Long.MAX_VALUE else route + map
}

val text = Formatter.formatFileSize(context, estimatedBytes(row))
```

Recommended preflight: _available bytes >= remaining download bytes + max(20% of remaining bytes, 100 MB)_. `NBNavigation` does not currently expose one combined storage-preflight result for route and map downloads, so the application should still use **StatFs** on the volume that contains the actual data directory. Check each volume separately if route and map data use different volumes. Insufficient storage can still surface through progress or callbacks as `FAILED`, `FAILED_STORAGE`, or a structured error.

```kotlin
val remaining = remainingBytes.coerceAtLeast(0L)
val reserve = maxOf(
    100L * 1024L * 1024L,
    (remaining * 0.20).roundToLong()
)
val required = if (remaining > Long.MAX_VALUE - reserve) Long.MAX_VALUE
               else remaining + reserve
val enoughSpace = StatFs(storageDir.absolutePath).availableBytes >= required
```
