A report came in on a map-based app: "zoom out and other users' territories disappear, and it stutters." It looked like slow loading. It seemed like waiting would fill it in. It didn't.
Three causes, all client bugs
The server was fine from the start.
① A fixed bbox independent of zoom. On camera movement the query range was hardcoded to delta = 0.05, requesting only center ±0.05°. That's about 8.8 × 11 km in Seoul. Screen area quadruples per zoom-out while the query range stays fixed, so below roughly z13 the screen edges were never even requested from the server. With a portrait screen, the vertical axis was already cut at z13.
So it's permanent omission, not a loading delay. Waiting never brings it — the request was never made.
② No debounce. The effect's key was camera position, so it re-ran every gesture frame. Uncancelled RPCs flooded out. iOS had a 400ms debounce + previous-Task cancellation from the start. Same feature, and only one side was missing it.
③ Rendering the whole cache. The local DB's observeAll() is the full table that accumulates until logout, and the screen was drawing all of it — building polygons and marker bitmaps even for off-screen territory. It gets heavier the longer the app is used.
This is where the diagnosis got confusing. Because the cache accumulates, territories that once happened to fall inside the fixed box stay visible. The rest never appear. That mix read as "some show, some haven't arrived yet" — i.e. a half-finished load. A partly-correct screen is harder to diagnose than a completely wrong one.
The fastest evidence — per-box counts via anon key
The fastest step in this diagnosis was calling the RPC directly with the anon key and comparing counts per box:
old box (city hall ±0.05°) → 0 rows
actual z11 viewport → 60 rows
nationwide → 180 rowsThe moment you see 0 vs 60, "the request range is the bug" is settled. Far faster than grinding through client logs. And since nationwide is 180, you also confirm LIMIT 500 isn't being hit yet.
Code
MapViewport.kt (new)
prefer cameraPositionState.projection?.visibleRegion?.latLngBounds
fall back to a Web-Mercator zoom formula if projection is null (pre-layout)
20% padding on bothTwo traps here:
- Latitude must be computed in Mercator y space. Otherwise the center drifts at high latitudes.
- Antimeridian crossing (
neLng < swLng) expands to -180..180. Pass it through as-is andST_MakeEnvelopemakes an empty envelope.
MapViewModel.loadVisibleArea(MapBBox)
cancel a single fetchJob / skip duplicates via loadedBBox·inFlightBBox⚠️ In refreshBBox's catch, always rethrow CancellationException. Otherwise a cancellation leaks into errorMessage and the spinner sticks. Rendering is limited to the viewport intersection, and polygon JSON parsing is memoized by row id + json hash.
Fix shipped: 0.11.2 / vc42. Apps: Plotta (Android) · iOS. This is also the app where R8 stripped the route field — iOS and Android implementing the same feature differently is the shared backdrop of both bugs.

Honestly
- I fixed three client bugs, and the underlying wide-zoom problem is still there. A single-screen query has
LIMIT 500, and production is only 180 rows total right now, so it just isn't reached. Grow the data and it breaks again. - I wrote the migration that relaxes that ceiling (
ORDER BY area_m2 DESC, id) and didn't apply it. All it does is make "which 500 get cut when 500 is exceeded" deterministic. Below 500 the behavior is identical. Zero perceptible change for one production DB touch — no reason to spend it now. - The real root fix isn't done. The priority is set — a zoom-based minimum-area filter (a 200m² polygon at z9 is under 1px, so don't send it at all), then geohash aggregation, then tile caching at tens-of-thousands scale. The first has the highest effect-per-change.
- The backdrop is that iOS and Android implemented the same feature differently. Like the debounce that was on only one side, whether there are other gaps needs a separate sweep.
Read "janky and missing" as a loading problem and you'll grind the client forever. Picture one screen in your app that "seems slow." Is it actually slow, or was it never requested? One count query with the anon key tells them apart.