public double score(StringWrapper s, StringWrapper t) {
        checkTrainingHasHappened(s, t);
        UnitVector sBag = asUnitVector(s);
        UnitVector tBag = asUnitVector(t);
        List<Similarity> similarities = new ArrayList<Similarity>(sBag.size());
        double sim = 0.0;
        int i = 0;
        for (Iterator<Token> ti = sBag.tokenIterator(); ti.hasNext(); i++) {
            Token tok = ti.next();
            int j = 0;
            for (Iterator<Token> tj = tBag.tokenIterator(); tj.hasNext(); j++) {
                Token tokJ = tj.next();
                double distItoJ = tokenDistance.score(tok.getValue(), tokJ.getValue());
                if (distItoJ >= tokenMatchThreshold) {
                    similarities.add(new Similarity(i, j, distItoJ * sBag.getWeight(tok) * tBag.getWeight(tokJ)));
                }
            }

        }

        /*
         * This could be O(sBag.size() * tBag.size() * log (sBag.size() * tBag.size())) in the worst case but usually
         * the threshold will make it much better and likely less than O(sBag.size() * tBag.size()) which is the current
         * complexity.
         */
        Collections.sort(similarities, Collections.reverseOrder());

        boolean[] sUsed = new boolean[sBag.size()];
        boolean[] tUsed = new boolean[tBag.size()];

        // enforce that each word is only used for one similarity, to make sure normalization works
        for (int k = 0; k < similarities.size(); k++) {
            Similarity similarity = similarities.get(k);
            if (sUsed[similarity.r1] || tUsed[similarity.r2])
                continue;
            sim += similarity.sim;
            sUsed[similarity.r1] = true;
            tUsed[similarity.r2] = true;
        }

        return sim;
    }