docs: @Override
  	public boolean callWithNextKey(final FlatJoinFunction<T1, T2, O> joinFunction, final Collector<O> collector) throws Exception {
  		if (!initialized) {
  			//first run, set iterators to first elements
  			it1Empty = !this.iterator1.nextKey();
  			it2Empty = !this.iterator2.nextKey();
  			initialized = true;
  		}
  
  		if (it1Empty && it2Empty) {
  			return false;
  		} else if (it2Empty) {
  			if (outerJoinType == OuterJoinType.LEFT || outerJoinType == OuterJoinType.FULL) {
  				joinLeftKeyValuesWithNull(iterator1.getValues(), joinFunction, collector);
  				it1Empty = !iterator1.nextKey();
  				return true;
  			} else {
  				//consume rest of left side
  				while (iterator1.nextKey()) {
  				}
  				it1Empty = true;
  				return false;
  			}
  		} else if (it1Empty) {
  			if (outerJoinType == OuterJoinType.RIGHT || outerJoinType == OuterJoinType.FULL) {
  				joinRightKeyValuesWithNull(iterator2.getValues(), joinFunction, collector);
  				it2Empty = !iterator2.nextKey();
  				return true;
  			} else {
  				//consume rest of right side
  				while (iterator2.nextKey()) {
  				}
  				it2Empty = true;
  				return false;
  			}
  		} else {
  			final TypePairComparator<T1, T2> comparator = super.pairComparator;
  			comparator.setReference(this.iterator1.getCurrent());
  			T2 current2 = this.iterator2.getCurrent();
  
  			// zig zag
  			while (true) {
  				// determine the relation between the (possibly composite) keys
  				final int comp = comparator.compareToReference(current2);
  
  				if (comp == 0) {
  					break;
  				}
  
  				if (comp < 0) {
  					//right key < left key
  					if (outerJoinType == OuterJoinType.RIGHT || outerJoinType == OuterJoinType.FULL) {
  						//join right key values with null in case of right or full outer join
  						joinRightKeyValuesWithNull(iterator2.getValues(), joinFunction, collector);
  						it2Empty = !iterator2.nextKey();
  						return true;
  					} else {
  						//skip this right key if it is a left outer join
  						if (!this.iterator2.nextKey()) {
  							//if right side is empty, join current left key values with null
  							joinLeftKeyValuesWithNull(iterator1.getValues(), joinFunction, collector);
  							it1Empty = !iterator1.nextKey();
  							it2Empty = true;
  							return true;
  						}
  						current2 = this.iterator2.getCurrent();
  					}
  				} else {
  					//right key > left key
  					if (outerJoinType == OuterJoinType.LEFT || outerJoinType == OuterJoinType.FULL) {
  						//join left key values with null in case of left or full outer join
  						joinLeftKeyValuesWithNull(iterator1.getValues(), joinFunction, collector);
  						it1Empty = !iterator1.nextKey();
  						return true;
  					} else {
  						//skip this left key if it is a right outer join
  						if (!this.iterator1.nextKey()) {
  							//if right side is empty, join current right key values with null
  							joinRightKeyValuesWithNull(iterator2.getValues(), joinFunction, collector);
  							it1Empty = true;
  							it2Empty = !iterator2.nextKey();
  							return true;
  						}
  						comparator.setReference(this.iterator1.getCurrent());
  					}
  				}
  			}
  
  			// here, we have a common key! call the join function with the cross product of the
  			// values
  			final Iterator<T1> values1 = this.iterator1.getValues();
  			final Iterator<T2> values2 = this.iterator2.getValues();
  
  			crossMatchingGroup(values1, values2, joinFunction, collector);
  			it1Empty = !iterator1.nextKey();
  			it2Empty = !iterator2.nextKey();
  			return true;
  		}
  	}