docs: def project_onto(objs, axis, min_gap_size=4.0):
      """
      Projects object bboxes onto the axis and return the
      unioned intervals and groups of objects in intervals.
      """
      if axis == "x":
          axis = 0
      if axis == "y":
          axis = 1
      axis_end = axis + 2
      if axis == 0:  # if projecting onto X axis
          objs.sort(key=lambda o: o.x0)
      else:
          objs.sort(key=lambda o: o.y0)
  
      intervals = []
      groups = []
  
      start_i = 0
      start = objs[0].bbox[axis]
      end = objs[0].bbox[axis_end]
  
      # Use _inf_bbox to trigger the last interval divide
      for o_i, o in enumerate(chain(objs, [_inf_bbox])):
  
          # Get current interval
          o_start = o.bbox[axis]
          o_end = o.bbox[axis_end]
  
          # start new interval when gap with previous end is big
          if o_start > end + min_gap_size:
  
              # Append new interval coordinates for children
              intervals.append((start, end))
  
              # Append child object group on page
              groups.append(objs[start_i:o_i])
  
              # Mark next obj list range
              start_i = o_i
              start = o_start
  
          # Always check to extend current interval to new end
          if o_end > end:
              end = o_end
          # else do nothing
      return intervals, groups