function loadNextTilePage(which, url, tile) {
  const cache = _ssCache[which];
  const nextPage = cache.nextPage[tile.id] || 0;
  const id = tile.id + ',' + String(nextPage);
  if (cache.loaded[id] || cache.inflight[id]) return;

  cache.inflight[id] = getBubbles(url, tile, response => {
    cache.loaded[id] = true;
    delete cache.inflight[id];
    if (!response) return;

    if (response.resourceSets[0].resources.length === maxResults) {
      // there are more bubbles than the response can fit: re-fetch using tile split into 4
      const split = tile.extent.split();
      loadNextTilePage(which, url, { id: tile.id + ',a', extent: split[0] });
      loadNextTilePage(which, url, { id: tile.id + ',b', extent: split[1] });
      loadNextTilePage(which, url, { id: tile.id + ',c', extent: split[2] });
      loadNextTilePage(which, url, { id: tile.id + ',d', extent: split[3] });
    }

    const features = response.resourceSets[0].resources.map(bubble => {
      const bubbleId = bubble.imageUrl;
      if (cache.points[bubbleId]) return null;  // skip duplicates

      // workaround for https://github.com/openstreetmap/iD/issues/10341#issuecomment-2275724738
      const loc = [
        bubble.lon || bubble.longitude,
        bubble.lat || bubble.latitude
      ];
      const d = {
        service: 'photo',
        loc: loc,
        key: bubbleId,
        imageUrl: bubble.imageUrl
          .replace('{subdomain}', bubble.imageUrlSubdomains[0]),
        ca: bubble.he || bubble.heading,
        captured_at: bubble.vintageEnd,
        captured_by: 'microsoft',
        pano: true,
        sequenceKey: null
      };

      cache.points[bubbleId] = d;

      return {
        minX: loc[0], minY: loc[1], maxX: loc[0], maxY: loc[1], data: d
      };

    }).filter(Boolean);

    cache.rtree.load(features);

    if (which === 'bubbles') {
      dispatch.call('loadedImages');
    }
  });
}
loadNextTilePage() load data for the next tile page in line.