Pracino Mobile Investigation
Map Marker Overlap
Code Audit
A math-first review of every place the current map marker pipeline can still produce visual or tap-target overlap after clustering, fan expansion, declutter, and camera transitions, now checked against the dense Candidates-map repro from Ostrava and extended with a bounded resolution policy.
01Executive summary
The layout is deterministic and its current tests are green, but several collision classes sit outside what those tests assert.
- The top issue is structural: fan and Cluster obstacles are never decluttered against each other.
- Two valid three-leg fans can sit 50.7 dp apart while their 101 dp center pills overlap by about 50.3 dp.
- Mini fallback and selected-pin immovability do not guarantee clearance; a mini can still overlap a fan, then expand into a much larger overlap when selected.
- The transient
collapseFanspath uses fan-leg obstacles while rendering a Cluster, which is not collision-equivalent for even fans. - Existing specs assert forms, offsets, budgets, and continuity—not pairwise rendered rectangles or native hit areas.
- The z14 Ostrava repro matches Findings 1, 2, and 4: obstacle classes are allowed to overlap before ordinary pill declutter runs.
- The smallest bounded policy keeps Circle Pins anchored, moves unselected fans atomically, and lets Cluster badges move before the existing pill pass.
Adjacent fan columns can overlap by roughly half a salary pill while remaining separate Supercluster outputs.
Several overlaps also share native Marker hit regions, so the visible top marker is not necessarily the marker that receives the tap.
The iOS top-pill sag and white-ring fill bleed are solved and are not counted below.
Use a Candidates-scoped macro-obstacle pass with a fixed-quadrant 46.4 dp envelope. It stays inside the current 49.4 dp transition budget without restoring Dock-style re-rolls.
02Where the guarantees stop
The pipeline prevents ordinary pills from colliding with known obstacles, but does not establish a global non-overlap invariant.
flowchart LR A[Map points] --> B[Supercluster radius] B --> C[Clusters and standalone pins] C --> D[Fan selection and marker budget] D --> E[Obstacle rectangles] E --> F[Greedy pill declutter] F --> G[Rendered markers] E -. no obstacle-vs-obstacle pass .-> H[Fan / fan and Cluster / Circle overlap] F -. unconditional fallback .-> I[Mini overlap] D -. transient form swap .-> J[Even-fan collapse overlap] G -. rendered size differs .-> K[Font scale and hit-area overlap]
The critical seam is obstacleRects: fan legs and Cluster pills claim space before ordinary salary pills, but those obstacles are never checked against one another. Circle Pins and selected pins are also immovable, so they join the same unresolved collision class.
03Ranked findings
The first four reproduce through the real Supercluster-to-layout path; later findings cover rendering contracts and boundary behavior.
| Rank | Finding | Trigger | Confidence | Covered by specs |
|---|---|---|---|---|
| 1 | Adjacent fans overlap | Two coincident groups just outside cluster radius | High | ✕ |
| 2 | Mini and selected pin overlap a fan | Odd fan plus boxed neighbouring salary pin | High | ✕ |
| 3 | Even-fan collapse puts Cluster over a pill | Zoom-out threshold with occupied center gap | High | ✕ |
| 4 | Rounded radius is below immovable footprint pairs | Half-zoom boundary | High | ✕ |
| 5 | Rendered footprints exceed layout estimates | Font scale 1.3 and wrapper padding | Medium-high | ✕ |
| 6 | Short fan viewports self-overlap | Landscape or split-screen height | High | ✕ |
| 7 | First-leaf fan anchor erodes separation | Near-epsilon leaves toward a neighbour | Medium-high | ✕ |
| 8 | Live zoom and grid crossings escape the model | Mid-gesture or budget-saturated crossing | Medium | ✕ |
| 9 | Coordinate-only updates are discarded | Same IDs and visual state, changed coordinates | High behavior / lower frequency | ✕ |
041. Adjacent fans overlap
Separate Supercluster outputs can still be much closer than the width of their rendered pills.
Concrete arrangement. At longitudeDelta = 0.02 on a 390 dp-wide viewport, place two exact-coordinate triples on the same latitude with centers 0.0026 degrees apart. Supercluster zoom 14 keeps them as separate clusters about 50.7 dp apart. Both become three-leg fans with center salary pills. A nine-character salary has a 101 dp estimated footprint, producing about 50.3 dp of overlap.
Math mechanism. Supercluster radius is a center-distance merge threshold, not a rectangle-clearance guarantee. Fans are added as obstacles, but no obstacle-vs-obstacle pass exists. Even fans can overlap row-to-row when their columns align.
Tap impact. The two center legs occupy the same hit region. Both are default-anchor Markers, so winner selection depends on render/platform ordering.
Evidence. map-marker-layout.ts:174-191, 401-431, 502-531; map-view.tsx:524-562; map-bounds.ts:3-8.
Spec gap. Current fan tests contain one fan only. Add a real Supercluster fixture with two odd fans just outside the radius and assert pairwise visual and hit rectangles.
Smallest fix seam. Resolve cluster/fan obstacles against one another before ordinary pin declutter. Do not treat a larger global radius as an isolated visual tweak; it changes density, fan onset, and marker churn.
052. Mini fallback and selection overlap
Fallback changes form, but it does not prove the fallback rectangle is safe.
Concrete arrangement. Use one exact-coordinate triple at longitude 0 and a standalone salary pin at longitude 0.0026 on a 390 x 844 viewport. The fan's center and outer legs block every full-pill nudge. The standalone pin collapses to a mini at its true anchor, but the 28 dp mini still intersects the 101 dp center fan pill by about 13.8 dp. Selecting it makes it immovable and full-size at the same coordinate, increasing overlap to about 50.3 dp.
Math mechanism. When every nudge fails, the mini rectangle is inserted unconditionally. Selected pills and Circle Pins are pre-claimed as immovable, but are not resolved against fan/Cluster obstacles.
Evidence. map-marker-layout.ts:227-281, 515-519; map-view.tsx:524-619.
Spec gap. The boxed-in test verifies only isMini; the selected test verifies priority. Neither asserts clearance. Add the integrated odd-fan-plus-neighbour case for mini and selected forms.
Smallest fix seam. Validate the mini rectangle before accepting fallback and define a deliberate last resort. Selection needs the same explicit obstacle policy; this does not imply restoring rejected Dock behavior.
063. Even-fan collapse overlaps
The transient badge and the obstacle model describe different shapes.
Concrete arrangement. A two-leg fan on a 390 x 844 viewport has 71.74 dp spacing, leaving an empty center gap. A salary pin just outside the Supercluster radius can sit at the same latitude in that gap. During zoom-out, collapseFans renders a 38 dp Cluster at the fan center but still declutters against the two invisible fan legs. At 50.7 dp horizontal separation, the Cluster and salary pill overlap by about 18.8 dp.
Math mechanism. Preserving fan obstacles during collapse is collision-equivalent for odd fans because they have a center leg. It is not equivalent for even fans.
Evidence. map-marker-layout.ts:351-363, 451-485, 502-531; map-view.tsx:186-228.
Spec gap. The existing two-leaf collapse test places its standalone pill far from the center and asserts identical pin layout. Add a just-outside-radius pill occupying the even-fan gap and assert against the shape that actually renders.
Smallest fix seam. Use the rendered badge footprint during collapse, then verify the resulting pin movement against the threshold-continuity gate.
074. Cluster radius boundary overlap
Integer zoom rounding reduces effective screen-space separation below several fixed marker pairs.
Concrete arrangement. At longitudeDelta = 360 / 2^14.5 on a 390 dp viewport, zoom rounds to 15 and the effective radius is about 32.3 dp. A non-coincident two-point Cluster and Circle Pin can remain separate at 32.64 dp center distance. Their 38 and 32 dp widths overlap by about 2.36 dp. Two minimum Clusters overlap more; larger count labels widen them further. Two Circle Pins narrowly avoid visual overlap, but their wrapper-sized hit areas can still overlap by roughly three dp.
Math mechanism. The documented square-root-of-two radius wobble makes the low-end radius smaller than Cluster/Cluster and Cluster/Circle half-width sums. Both elements are immovable in the current sweep.
Evidence. map-bounds.ts:3-8, 32-39; map-view.tsx:296-304; map-marker-layout.ts:174-243; map-markers.tsx:142-180, 419-424.
Spec gap. Pure layout fixtures bypass Supercluster. Add an integrated q = n + 0.5 case and distinguish visible rectangles from native hit rectangles.
Smallest fix seam. Resolve immovable pairs locally or explicitly choose tap priority; avoid treating CLUSTER_RADIUS as a one-number fix.
085. Rendered footprints exceed estimates
The pure geometry models nominal text, while the component supports larger rendered text and extra wrapper space.
Concrete arrangement. Place salary pills at the first separation the estimated rectangles accept, then use the supported 1.3 font scale. Layout assumes 32 dp height and eight dp per character. Rendering adds four dp of top/right wrapper space and allows 30 percent text growth. Bundled-font measurement indicates 58 000 Kc grows from about 67 px at 13 px to 86 px at 16.9 px; the resulting outer marker is roughly 117 dp rather than the 101 dp estimate.
Math mechanism. The three-dp collision gap cannot absorb the difference between nominal layout constants and maximum supported rendering. Cluster count text has the same scale mismatch.
Evidence. map-marker-layout.ts:42-63, 174-191; map-markers.tsx:142-180, 333-355, 419-448; shared text.tsx:7-16; theme index.ts:8-11.
Confidence note. The contract mismatch is definite; the exact 117 dp figure comes from the bundled font rather than a React Native onLayout capture.
Smallest fix/spec. Measure marker components at font scale 1.3 and feed conservative maximum dimensions into pure layout. Disabling map-label scaling is smaller code but is an accessibility decision, not a neutral geometry fix.
096. Short viewports self-overlap
Fan spacing scales with viewport height but has no minimum tied to the rendered leg.
Concrete arrangement. At 390 dp landscape height, fan step is 33.15 dp. Base 32 dp pills have only 1.15 dp clearance; an availability dot extends four dp upward and overlaps the pill above by about 2.85 dp. At maximum font scale, the pills themselves can exceed the step. In a five-leg fan, an inner same-side Marker container reaches into the outer pill and its less-negative z-index can win the overlapping tap region. Below 376.5 dp height, the above-leg spine calculation receives a negative height.
Math mechanism. Spacing is fixed at 8.5 percent of viewport height, while spine and collision assumptions use exactly 32 dp. Same-side Marker containers intentionally extend from their pill back to the shared anchor.
Evidence. map-marker-layout.ts:17-24, 120-134; map-markers.tsx:29-40, 184-233, 313-332.
Spec gap. Both suites use a 1000 dp viewport. Add 360/390 dp cases with four/five legs, availability dots, max-scale height, and explicit tap priority.
Smallest fix seam. Clamp step to maximum rendered leg height plus clearance, or reshape/reduce fans on short viewports.
107. First-leaf fan anchoring
The fan can move away from the aggregate coordinate on which Supercluster separation was based.
Concrete arrangement. Put two cluster centers just outside the radius, then order their first leaves toward one another within the allowed coincidence boxes. The coincidence predicate accepts independent spans just below 0.0005 degrees and anchors every leg at leaves[0]. At delta 0.02 on a 390 dp viewport, each fan can shift about 9.75 dp horizontally; two fans can close by almost 19.5 dp before pill widths are considered.
Math mechanism. Supercluster clearance applies to aggregate/projected feature geometry. Fan obstacles are later relocated to an arbitrary leaf, making overlap dependent on leaf order.
Evidence. map-marker-layout.ts:120-135, 286-296, 413-431, 508-519.
Spec gap. Current fan fixtures put the first leaf at the cluster coordinate. Add a leaf-order permutation test near epsilon with a neighbouring output feature.
Smallest fix seam. Anchor to stable cluster geometry or a deterministic centroid, then characterize the visual/tap transition before changing existing behavior.
118. Transition boundaries are not collision-tested
Settled-frame continuity is covered; interpolated camera frames and actual grid crossings are not.
Concrete arrangement. During zoom-out, map-coordinate distances contract continuously while fixed marker footprints are not re-decluttered until settle. At a high-density grid crossing near the 120-marker budget, query population and center-distance fan priority can change, swapping one coincident Cluster from badge to fan and exposing the earlier collision classes abruptly.
Math mechanism. Declutter is settle-scoped. Grid snapping deliberately freezes layout content until the query key changes; the boundary can then change feature set, fan selection, and budget discontinuously. Pure same-zoom pan within one query is translation-invariant and did not reveal a separate collision mechanism.
Evidence. map-marker-layout.ts:33-40, 406-431; map-view.tsx:178-228, 322-342, 498-513; map-bounds.ts:41-71.
Spec gap. Transition specs compare settled regions with fixed features. Add interpolated rectangle checks and just-before/after real Supercluster queries across one bbox grid line under a saturated fan budget.
Smallest fix seam. Pin the boundary behavior first. Live per-frame React declutter conflicts with the marker-churn constraints and should not be the first implementation response.
129. Coordinate-only updates stay stale
The hook can prevent corrected coordinates from ever reaching Supercluster.
Concrete arrangement. If an unchanged ordered point set receives only latitude/longitude updates, useMapBrowsing reuses the previous array. A formerly stacked set can remain fanned or overlapping at stale coordinates; a new collision can also remain invisible until another visual field, ID, order, or count changes.
Math mechanism. Structural equality checks ID, salary, availability, and unlock state, but omits both coordinates. The Supercluster index therefore does not rebuild.
Evidence. use-map-browsing.ts:226-254, 481-500; map-view.tsx:296-321.
Spec gap. The scoped suites test the pure layout only. Add a hook rerender test for coordinate-only updates.
Smallest fix. Include latitude and longitude in the structural equality check.
13Ostrava repro attribution
The supplied Candidates-map scene is a high-density normal case, and all three visible classes fall out of the current obstacle math without a platform-specific explanation.
The scene renders 74 native markers, leaving 46 slots below the 120-marker cap. Fan-budget saturation and pin truncation are therefore not causing the overlap. With hidden candidates scattered by about 500 m server-side, many outputs land far enough apart to avoid clustering but still close enough for their fixed-size screen rectangles to intersect.
At z14 fan mode on a 390 dp-wide viewport, the rounded Supercluster radius supplies roughly 50.2–64.6 dp of screen separation. A typical 101 dp salary pill spans roughly 290–370 m at the same zoom, while the cluster radius is only about 185 m near Ostrava. That mismatch makes near-coincident scatter an expected collision source rather than a boundary fixture.
| Visible class | Attribution | Required separation | Allowed overlap | Confidence |
|---|---|---|---|---|
| 2-count Cluster over salary pill | Finding 1, with Finding 4 supplying the radius premise | 69.5 dp | 4.9–19.3 dp; probe: 18.8 dp | High |
| Circle Pin over salary pill | Finding 2, with Finding 4 supplying the radius premise | 66.5 dp | 1.9–16.3 dp; probe: 15.8 dp | High |
| Salary pill behind salary pill | Finding 1, amplified by Finding 7 | 101 dp | 36.4–50.8 dp; probe: 50.3 dp | High |
The direct probes use the real Supercluster-to-layout seam. No new Android/iOS rendering defect is needed: the matching result on both platforms is what the pure layout model predicts.
Findings 1, 2, and 4 should be treated as routine Candidates-map behavior under the real privacy scatter, not as rare boundary conditions.
14Minimal obstacle policy
Resolve rendered macro obstacles first, preserve the user's active spatial references, then reuse the current salary-pill sweep.
| Element class | Movement policy | Why |
|---|---|---|
| Selected standalone marker | Hard anchor; claims first | It is the user's active spatial reference |
| Fan containing the selected point | Hard-anchor the entire fan | Legs and spine must remain one column |
| Circle Pins | Stay true-to-anchor | Preserves the existing exact anchored-circle transition contract |
| Unselected fan | Move only as one atomic column | Solves fan/fan and Circle/fan collisions without distorting the fan |
| Cluster badge | May move inside the bounded envelope | It is an aggregate control rather than a precise person coordinate |
| Ordinary salary pill | Keep the current nudge/collapse pass | It already behaves once macro obstacles claim truthful rectangles |
| Mini fallback | Accept only after collision testing | The current unconditional anchor can recreate Finding 2 |
The claim order is:
- Selected standalone marker, or the whole fan containing the selected point.
- Circle Pins at their true anchors.
- Unselected fan columns in stable fan-key order.
- Cluster badges, higher
point_countfirst and then stable ID. - Ordinary salary pills in their existing availability priority.
- Collision-tested mini fallback.
Each fan placement is transactional: shift every leg rectangle by the same candidate offset, accept only if all legs clear the already claimed space, then claim the entire fan. During collapseFans, resolve the compact Cluster rectangle that actually renders rather than invisible fan-leg rectangles.
Fans move around Circle Pins; Circle Pins do not move. The current zero-displacement circle assertion can stay green.
If a connected component cannot fit inside the bounded envelope, return a residual-collision diagnostic. Do not silently add farther slots.
15Transition budget without Dock
A one-quadrant candidate set bounds every re-placement, so the resolver cannot jump from one side of an anchor to the other.
Use one fixed quadrant of the existing nudge envelope for each movable macro class:
- x candidates:
0or28with one fixed sign; - y candidates:
0,18, or at most37with one fixed sign; - combinations stay in that quadrant;
- fan/fan placement may also try an atomic y interleave of
min(37, stepPx / 2), about 35.9 dp in this scene.
The quadrant can be class-fixed in a pure implementation—for example, fans positive x/y and Clusters negative x/y—or persisted immutably per marker key. It must not choose a sign from the currently nearest obstacle, because a new neighbour could reverse the sign on the next settle.
The candidate-set diameter is hypot(28, 37) = 46.4 dp, below the current 46.4 + 3 = 49.4 dp transition allowance. No offset grows with fan length, and no marker can re-roll across the anchor as Dock did.
The existing bidirectional pill ladder is not safe for these macro classes: it contains both y signs, so a legal placement can flip 74 dp from -37 to +37 even though each position is individually close to the anchor.
16Implementation choices
The first option matches the geometry to the Candidates map's approximate-location semantics without changing the Offers map.
| Rank | Option | Risk | Effort | Coverage |
|---|---|---|---|---|
| 1 | Candidates-scoped atomic macro resolver | Medium | Medium | All three reported classes; preserves Circle anchors and budget |
| 2 | Global atomic macro resolver | Medium-high | Medium-low | Same geometry, but moves Offers-map aggregates too |
| 3 | Candidates-specific upstream aggregation | High | Low-medium | Reduces density but gives no rectangle-level guarantee |
1. Candidates-scoped atomic macro resolver — recommended
- Add a Candidates layout policy to
map-marker-layout.ts:351-379. - Thread browsing role through
browsing-screen.tsx:479-490,map-view.tsx:65-84, and the layout call atmap-view.tsx:501-513. - Introduce typed macro obstacles at
map-marker-layout.ts:194-283, 517-555and apply accepted fan/Cluster coordinates atmap-marker-layout.ts:451-500. - No new Marker primitive is needed:
PinMarkerandClusterMarkeralready consume coordinates atmap-markers.tsx:208-245, 288-306. - Add Cluster/fan, Circle/fan, fan/fan, mini-fallback, and infeasible-component cases. Keep
map-marker-layout-transitions.spec.ts:372-377exact for Circle Pins and assert macro displacement under the existing budget at lines262-305, 395-405.
2. Global atomic macro resolver
Apply the same resolver directly to both map roles at map-marker-layout.ts:194-283, 451-555. This removes prop plumbing but changes truthful offer-marker behavior without evidence that the Offers map needs the same tradeoff.
3. Candidates-specific upstream aggregation
Adjust the role-specific Supercluster policy at map-view.tsx:296-321, with shared constants at map-bounds.ts:3-12. This is an honest fallback when the bounded resolver reports infeasible components, but it changes counts, fan timing, tap-to-zoom behavior, and churn while still not proving rectangle clearance.
Promote the direct 18.8 dp Cluster/pill, 15.8 dp Circle/pill, and 50.3 dp pill/pill probes into pairwise visual and hit-rectangle assertions before implementing the resolver.
17Geometry to preserve
Several odd-looking pieces are compensating for known native-map behavior and should not be simplified casually.
Preserve collapsable={false} on fan columns and the white ring wrapper; they prevent the known iOS sag and fill bleed.
Pixel offsets and asymmetric Marker anchors keep fan spacing constant through camera motion.
Per-leg spine segments avoid cross-marker z-order dependence on Android.
Negative distance z-index compensates for overlapping transparent leg containers, even though it is insufficient at short heights.
Grid-snapped padded bounds, anchor freezing, cluster radius, and the marker budget are performance and continuity controls—not standalone visual constants.
Circle Pin and selected-pin immovability create collision blind spots, but moving them without transition evidence repeats the rejected Dock failure mode.
Keeping fan obstacles during collapseFans is also load-bearing for crossing continuity, despite causing the even-fan defect. Any correction must measure both clearance and displacement across the threshold.
18Verification and limits
What was checked, and what this code-only audit deliberately did not claim.
- Read current committed layout, marker rendering, map view, hook, cluster bounds, and both layout specs
- Excluded the two known solved rendering regressions
- Ran both marker-layout suites: 26 of 26 tests passed
- Exercised boundary arrangements through the real Supercluster-to-layout path
- Attributed the supplied z14 Candidates-map scene to Findings 1, 2, and 4 with quantified overlap envelopes
- Proved the recommended macro movement set remains inside the 49.4 dp transition budget
- Included native hit-area competition where the component geometry supports it
- Device or simulator verification
- Exact React Native component measurement at font scale 1.3
- Runtime frequency analysis against production coordinate distributions
This was intentionally read-only. No marker code, tests, commits, device sessions, or issue-tracker state changed.