def run
      return if _clusters.empty?
      warn "warning: running aggregation on many clusters (#{_clusters.size})" if _clusters.size > 100

      if DENSITY_BIAS_FACTOR
        @min_density, @max_density = _clusters.collect(&:density).minmax
      end

      # build the set of pairs (n²/2)
      pairs  = []
      source = _clusters.dup
      while left = source.pop
        source.each do |right| 
          pairs << _build_pair(left, right, _fence)
        end
      end

      # pop & merge
      while pairs.any?
        best_pair = pairs.min
        break if best_pair.distance > min_distance

        # remove the closest pair
        left, right = best_pair.values
        left_id  = left.id
        right_id = right.id

        # remove pairs containing one of the items
        pairs.delete_if { |p| p.has?(left) || p.has?(right) }

        # merge clusters
        left.merge(right)
        _clusters.delete_if { |c| c.id == right_id }
        _updated_clusters.remove right
        _dropped_clusters.add    right
        _updated_clusters.add    left

        # create new pairs
        _clusters.each do |cluster|
          next if cluster.id == left_id
          pairs << _build_pair(left, cluster, _fence)
        end
      end
      nil
    end
def run
      return if _clusters.empty?
      warn "warning: running aggregation on many clusters (#{_clusters.size})" if _clusters.size > 100

      if DENSITY_BIAS_FACTOR
        @min_density, @max_density = _clusters.collect(&:density).minmax
      end

      # build the set of pairs (n²/2)
      pairs  = []
      source = _clusters.dup
      while left = source.pop
        source.each do |right| 
          pairs << _build_pair(left, right, _fence)
        end
      end

      # pop & merge
      while pairs.any?
        best_pair = pairs.min
        break if best_pair.distance > min_distance

        # remove the closest pair
        left, right = best_pair.values
        left_id  = left.id
        right_id = right.id

        # remove pairs containing one of the items
        pairs.delete_if { |p| p.has?(left) || p.has?(right) }

        # merge clusters
        left.merge(right)
        _clusters.delete_if { |c| c.id == right_id }
        _updated_clusters.remove right
        _dropped_clusters.add    right
        _updated_clusters.add    left

        # create new pairs
        _clusters.each do |cluster|
          next if cluster.id == left_id
          pairs << _build_pair(left, cluster, _fence)
        end
      end
      nil
    end
- clusters (Array): list of points to aggregate
 - fence (Tile): clusters outside this will not be aggregated
 - ratio (0..1): minimum distance between clusters after aggregation,
   as a ratio of the tile diagonal