docs: public Result run(Database db, Relation<O> relation) {
      DistanceQuery<O> dq = db.getDistanceQuery(relation, getDistanceFunction());
      ArrayDBIDs ids = DBIDUtil.ensureArray(relation.getDBIDs());
      final int size = ids.size();
  
      if(size > 0x10000) {
        throw new AbortException("This implementation does not scale to data sets larger than " + 0x10000 + " instances (~17 GB RAM), which results in an integer overflow.");
      }
      if(Linkage.SINGLE.equals(linkage)) {
        LOG.verbose("Notice: SLINK is a much faster algorithm for single-linkage clustering!");
      }
  
      // Compute the initial (lower triangular) distance matrix.
      double[] scratch = new double[triangleSize(size)];
      DBIDArrayIter ix = ids.iter(), iy = ids.iter();
      // Position counter - must agree with computeOffset!
      int pos = 0;
      boolean square = Linkage.WARD.equals(linkage) && !getDistanceFunction().isSquared();
      for(int x = 0; ix.valid(); x++, ix.advance()) {
        iy.seek(0);
        for(int y = 0; y < x; y++, iy.advance()) {
          scratch[pos] = dq.distance(ix, iy);
          // Ward uses variances -- i.e. squared values
          if(square) {
            scratch[pos] *= scratch[pos];
          }
          pos++;
        }
      }
  
      // Initialize space for result:
      double[] height = new double[size];
      Arrays.fill(height, Double.POSITIVE_INFINITY);
      // Parent node, to track merges
      // have every object point to itself initially
      ArrayModifiableDBIDs parent = DBIDUtil.newArray(ids);
      // Active clusters, when not trivial.
      Int2ReferenceMap<ModifiableDBIDs> clusters = new Int2ReferenceOpenHashMap<>();
  
      // Repeat until everything merged, except the desired number of clusters:
      final int stop = size - numclusters;
      FiniteProgress prog = LOG.isVerbose() ? new FiniteProgress("Agglomerative clustering", stop, LOG) : null;
      for(int i = 0; i < stop; i++) {
        double min = Double.POSITIVE_INFINITY;
        int minx = -1, miny = -1;
        for(int x = 0; x < size; x++) {
          if(height[x] < Double.POSITIVE_INFINITY) {
            continue;
          }
          final int xbase = triangleSize(x);
          for(int y = 0; y < x; y++) {
            if(height[y] < Double.POSITIVE_INFINITY) {
              continue;
            }
            final int idx = xbase + y;
            if(scratch[idx] < min) {
              min = scratch[idx];
              minx = x;
              miny = y;
            }
          }
        }
        assert (minx >= 0 && miny >= 0);
        // Avoid allocating memory, by reusing existing iterators:
        ix.seek(minx);
        iy.seek(miny);
        // Perform merge in data structure: x -> y
        // Since y < x, prefer keeping y, dropping x.
        height[minx] = min;
        parent.set(minx, iy);
        // Merge into cluster
        ModifiableDBIDs cx = clusters.get(minx);
        ModifiableDBIDs cy = clusters.get(miny);
        int sizex = 1, sizey = 1; // cluster sizes, for averaging
        if(cy == null) {
          cy = DBIDUtil.newHashSet();
          cy.add(iy);
        }
        else {
          sizey = cy.size();
        }
        if(cx == null) {
          cy.add(ix);
        }
        else {
          sizex = cx.size();
          cy.addDBIDs(cx);
          clusters.remove(minx);
        }
        clusters.put(miny, cy);
  
        // Update distance matrix. Note: miny < minx
  
        // Implementation note: most will not need sizej, and could save the
        // hashmap lookup.
        final int xbase = triangleSize(minx), ybase = triangleSize(miny);
        // Write to (y, j), with j < y
        for(int j = 0; j < miny; j++) {
          if(height[j] < Double.POSITIVE_INFINITY) {
            continue;
          }
          final DBIDs idsj = clusters.get(j);
          final int sizej = (idsj == null) ? 1 : idsj.size();
          scratch[ybase + j] = linkage.combine(sizex, scratch[xbase + j], sizey, scratch[ybase + j], sizej, min);
        }
        // Write to (j, y), with y < j < x
        for(int j = miny + 1; j < minx; j++) {
          if(height[j] < Double.POSITIVE_INFINITY) {
            continue;
          }
          final int jbase = triangleSize(j);
          final DBIDs idsj = clusters.get(j);
          final int sizej = (idsj == null) ? 1 : idsj.size();
          scratch[jbase + miny] = linkage.combine(sizex, scratch[xbase + j], sizey, scratch[jbase + miny], sizej, min);
        }
        // Write to (j, y), with y < x < j
        for(int j = minx + 1; j < size; j++) {
          if(height[j] < Double.POSITIVE_INFINITY) {
            continue;
          }
          final DBIDs idsj = clusters.get(j);
          final int sizej = (idsj == null) ? 1 : idsj.size();
          final int jbase = triangleSize(j);
          scratch[jbase + miny] = linkage.combine(sizex, scratch[jbase + minx], sizey, scratch[jbase + miny], sizej, min);
        }
        LOG.incrementProcessed(prog);
      }
      LOG.ensureCompleted(prog);
  
      // Build the clustering result
      final Clustering<Model> dendrogram = new Clustering<>("Hierarchical-Clustering", "hierarchical-clustering");
      for(int x = 0; x < size; x++) {
        if(height[x] < Double.POSITIVE_INFINITY) {
          DBIDs cids = clusters.get(x);
          if(cids == null) {
            ix.seek(x);
            cids = DBIDUtil.deref(ix);
          }
          Cluster<Model> cluster = new Cluster<>("Cluster", cids);
          dendrogram.addToplevelCluster(cluster);
        }
      }
  
      return dendrogram;
    }