@Override
  public NavigableMap<Long, Long> findGtEqIds(Dictionary<Long> otherDict) {
    NavigableMap<Long, Long> res = new TreeMap<>();

    if (otherDict instanceof ArrayCompressedLongDictionary) {
      CompressedLongArray<?> otherSortedValues =
          ((ArrayCompressedLongDictionary) otherDict).sortedValues;
      if (sortedValues.size() == 0 || otherSortedValues.size() == 0) return res;

      // good case: we can traverse the two arrays simultaneously and only need to store O(1) in
      // memory.
      int posThis = 0;
      int posOther = 0;
      long decompressedThis = sortedValues.get(posThis);
      long decompressedOther = otherSortedValues.get(posOther);

      while (decompressedThis < decompressedOther && posThis < sortedValues.size() - 1)
        decompressedThis = sortedValues.get(++posThis);

      boolean doBreak = false;
      while (!doBreak) {

        while (decompressedThis > decompressedOther) { // "inner while loop"
          if (posOther == otherSortedValues.size() - 1) {
            while (posThis < sortedValues.size()) res.put((long) posThis++, (long) -(posOther + 1));

            doBreak = true;
            break;
          }
          decompressedOther = otherSortedValues.get(++posOther);
        }

        if (!doBreak) {
          if (decompressedThis == decompressedOther) {
            res.put((long) posThis, (long) posOther);
          } else {
            // we know: decompressedOther > decompressedThis
            // but: in previous run of "inner while loop", decompressedOther < decompressedThis.
            // so: mark posThis as being greater than the previous posOther, restore position in
            // other dict and proceed
            // 'this' one forward. If in the next loop, decompressedThis is still >= the next item
            // in other, then we
            // will visit this very same execution again right away.
            decompressedOther = otherSortedValues.get(--posOther);

            res.put((long) posThis, (long) -(posOther + 1));
          }

          if (posThis == sortedValues.size() - 1) doBreak = true;
          else decompressedThis = sortedValues.get(++posThis);
        }
      }

    } else if (otherDict instanceof ConstantLongDictionary) {
      long otherId = ((ConstantLongDictionary) otherDict).getId();
      long otherValue = ((ConstantLongDictionary) otherDict).getDecompressedValue();

      Long ourGtEqId = findGtEqIdOfValue(otherValue);
      if (ourGtEqId != null) {
        if (ourGtEqId < 0) {
          ourGtEqId = -(ourGtEqId + 1);
          otherId = -(otherId + 1);
        }
        res.put(ourGtEqId, otherId);

        if (otherId > 0) otherId = -(otherId + 1);
        for (long ourId = ourGtEqId + 1; ourId < sortedValues.size(); ourId++)
          res.put(ourId, otherId);
      }
    } else if (otherDict instanceof EmptyLongDictionary) {
      // noop.
    } else {
      // Bad case: decompress whole array.
      long[] decompressedValues = sortedValues.decompressedArray();
      for (int i = 0; i < decompressedValues.length; i++) {
        Long otherId = otherDict.findLtEqIdOfValue(decompressedValues[i]);
        if (otherId == null) break;
        res.put((long) i, otherId);
      }
    }

    return res;
  }
  @Override
  public NavigableMap<Long, Long> findLtEqIds(Dictionary<Long> otherDict) {
    NavigableMap<Long, Long> res = new TreeMap<>();

    if (otherDict instanceof ArrayCompressedLongDictionary) {
      CompressedLongArray<?> otherSortedValues =
          ((ArrayCompressedLongDictionary) otherDict).sortedValues;
      if (sortedValues.size() == 0 || otherSortedValues.size() == 0) return res;

      // good case: we can traverse the two arrays simultaneously and only need to store O(1) in
      // memory.
      int posThis = 0;
      int posOther = 0;
      long decompressedThis = sortedValues.get(posThis);
      long decompressedOther = otherSortedValues.get(posOther);

      boolean doBreak = false;
      while (!doBreak) {

        while (decompressedThis > decompressedOther) {
          if (posOther == otherSortedValues.size() - 1) {
            doBreak = true;
            break;
          }
          decompressedOther = otherSortedValues.get(++posOther);
        }

        if (!doBreak) {
          if (decompressedThis == decompressedOther) res.put((long) posThis, (long) posOther);
          else
            // we know: decompressedThis < decompressedOther
            res.put((long) posThis, (long) -(posOther + 1));

          if (posThis == sortedValues.size() - 1)
            // done processing
            doBreak = true;
          else
            // move this one further
            decompressedThis = sortedValues.get(++posThis);
        }
      }
    } else if (otherDict instanceof ConstantLongDictionary) {
      long otherId = ((ConstantLongDictionary) otherDict).getId();
      long otherValue = ((ConstantLongDictionary) otherDict).getDecompressedValue();

      Long ourLtEqId = findLtEqIdOfValue(otherValue);
      if (ourLtEqId != null) {
        if (ourLtEqId < 0) {
          ourLtEqId = -(ourLtEqId + 1);
          otherId = -(otherId + 1);
        }
        res.put(ourLtEqId, otherId);

        if (otherId > 0) otherId = -(otherId + 1);
        for (long ourId = 0; ourId < ourLtEqId; ourId++) res.put(ourId, otherId);
      }
    } else if (otherDict instanceof EmptyLongDictionary) {
      // noop.
    } else {
      // Bad case: decompress whole array.
      long[] decompressedValues = sortedValues.decompressedArray();
      for (int i = 0; i < decompressedValues.length; i++) {
        Long otherId = otherDict.findGtEqIdOfValue(decompressedValues[i]);
        if (otherId == null) break;
        res.put((long) i, otherId);
      }
    }

    return res;
  }
  @Override
  public NavigableMap<Long, Long> findEqualIds(Dictionary<Long> otherDict) {
    NavigableMap<Long, Long> res = new TreeMap<>();

    if (otherDict instanceof ArrayCompressedLongDictionary) {
      CompressedLongArray<?> otherSortedValues =
          ((ArrayCompressedLongDictionary) otherDict).sortedValues;
      if (sortedValues.size() == 0 || otherSortedValues.size() == 0) return res;

      // good case: we can traverse the two arrays simultaneously and only need to store O(1) in
      // memory.
      int posThis = 0;
      int posOther = 0;
      long decompressedThis = sortedValues.get(posThis);
      long decompressedOther = otherSortedValues.get(posOther);
      while (posThis < sortedValues.size() && posOther < otherSortedValues.size()) {
        // move 'posThis' right until decompressedThis is >= decompressedOther
        while (posThis < sortedValues.size() - 1 && decompressedThis < decompressedOther)
          decompressedThis = sortedValues.get(++posThis);

        // move 'posOther' right until decompressedOther is >= decompressedThis
        while (posOther < otherSortedValues.size() - 1 && decompressedOther < decompressedThis)
          decompressedOther = otherSortedValues.get(++posOther);

        // validate if we have a match
        if (decompressedThis == decompressedOther) {
          res.put((long) posThis++, (long) posOther++);
          if (posThis < sortedValues.size() && posOther < otherSortedValues.size()) {
            decompressedThis = sortedValues.get(posThis);
            decompressedOther = otherSortedValues.get(posOther);
          }
        } else if ((posThis == sortedValues.size() - 1 && decompressedThis < decompressedOther)
            || (posThis == sortedValues.size() - 1 && posOther == otherSortedValues.size() - 1))
          break;
      }
    } else if (otherDict instanceof ConstantLongDictionary) {
      long otherId = ((ConstantLongDictionary) otherDict).getId();
      long otherValue = ((ConstantLongDictionary) otherDict).getDecompressedValue();

      try {
        long ourId = findIdOfValue(otherValue);
        res.put(ourId, otherId);
      } catch (IllegalArgumentException e) {
        // swallow, return empty dict.
      }
    } else if (otherDict instanceof EmptyLongDictionary) {
      // noop.
    } else {
      // Bad case: decompress whole array (should not happen, though)
      long[] decompressedValues = sortedValues.decompressedArray();
      Long[] otherIds =
          otherDict.findIdsOfValues(
              LongStream.of(decompressedValues).mapToObj(Long::valueOf).toArray(l -> new Long[l]));
      for (int i = 0; i < decompressedValues.length; i++) {
        Long otherId = otherIds[i];
        if (otherId != -1L) res.put((long) i, otherId);
      }
    }

    return res;
  }