docs: def bucketIterator(elements, buckets):
    """
    For each bucket in buckets, yield it and any elements that overlap it.
  
    :param elements: the genomic intervals to place into the buckets. Must be
                     sorted by chromosome and start index. This could be a
                     list, or an iterator.
    :param buckets: the buckets into which genomic intervals should be binned.
                    Must be sorted by chromosome and start index. This could be
                    a list, or an iterator
    :return: iterator that will yeild a tuple of 1 bucket and 1 list of elements
             in the bucket for each call to __next__().
    """
    def check_sorted(current, previous):
      if (previous is not None) and \
         ((previous.chrom > current.chrom) or
          ((previous.chrom == current.chrom) and
           (previous.start > current.start))):
        raise GenomicIntervalError("elements not sorted. Saw " +
                                   str(previous) + " before " + str(current))
  
    def updateOpen(openHeap, elementIterator, bucketChrom,
                   bucketStart, bucketEnd):
      """
      Drop elements from heap which start earlier than current bucket.
  
      Update the open heap so that it contains only elements that end after the
      start of the current bucket. Note that the heap may already contain some
      elements that start after the end of the current bucket, if a previous
      bucket ended after the end of this one and brought them into the set.
  
      :param openHeap: a min heap of elements; uses the default sorting order
                       for the genomic intervals, which is by end index. This
                       is what we're updating.
      :param elementIterator: an iterator from which we will pull new elements.
                              Elements yielded by this iterator must be sorted
                              by start index. Must be 'peakable'
      :param bucketChrom: the chromosome of the current bucket.
      :param bucketStart: the start index of the current bucket.
      :param bucketEnd: the end index of the current bucket.
      """
      # first, we're going to pop elements from the heap which can no longer
      # overalp this or any future buckets. Buckets are sorted by start, so
      # we'll never see another bucket that starts earlier than this one --
      # hence any elements that end before the start of this bucket will never be
      # used again and can be dropped. Elements in the heap are ordered by end
      # index, so once we reach an element in the heap that does not end before
      # the start of this bucket, we are sure that no others will come after it
      # which do end before the start of this bucket. So we can stop dropping.
      while len(openHeap) > 0 and ((openHeap[0].chrom < bucketChrom) or
                                   ((openHeap[0].chrom == bucketChrom) and
                                   (openHeap[0].end <= bucketStart))):
        heappop(openHeap)
  
      # now we're going to add new elements from the iterator to the heap. As
      # we know that elements in the iterator are sorted by start index, we know
      # that once we see an element that has a start index greater than the end
      # of this bucket, we can stop -- everything else after it will also start
      # after the end of this bucket.
      while (elementIterator.peek() is not None) and \
            ((elementIterator.peek().chrom < bucketChrom) or
             ((elementIterator.peek().chrom == bucketChrom) and
              (elementIterator.peek().start < bucketEnd))):
        e = elementIterator.__next__()
        # if e falls before this bucket, we can skip it; buckets are sorted by
        # start, so no other buckets start earlier than this one and so it
        # cannot intersect any others.
        if (e.chrom < bucketChrom) or \
           (e.chrom == bucketChrom and e.end <= bucketStart):
           continue
        # now we know e intersects this bucket..
        heappush(openHeap, e)
  
    openElems = []
    prevBucket = None
    elementIter = AutoApplyIterator(elements, check_sorted)
    for bucket in buckets:
      # make sure the buckets are sorted by start index
      if prevBucket is not None and ((bucket.chrom < prevBucket.chrom) or
                                     (bucket.chrom == prevBucket.chrom and
                                      bucket.start < prevBucket.start)):
        raise GenomicIntervalError("regions-of-interest are not sorted. Saw " +
                                   str(prevBucket) + " before " + str(bucket))
      updateOpen(openElems, elementIter, bucket.chrom, bucket. start, bucket.end)
  
      # be careful here not to leak a reference to the heap; if the caller
      # decides to mess with that list, it'll screw us up. Anyway, we need a
      # final check here to make sure we trim off any elements that exceed the
      # end of this bucket.
      yield bucket, [x for x in openElems if x.start < bucket.end]
      prevBucket = bucket