diff --git a/lucene/core/src/java/org/apache/lucene/search/AutomatonQuery.java b/lucene/core/src/java/org/apache/lucene/search/AutomatonQuery.java
index 7fb155d78e..91a667438f 100644
--- a/lucene/core/src/java/org/apache/lucene/search/AutomatonQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/AutomatonQuery.java
@@ -55,7 +55,7 @@ public class AutomatonQuery extends MultiTermQuery {
 
   /**
    * Create a new AutomatonQuery from an {@link Automaton}.
-   * 
+   *
    * @param term Term containing field and possibly some pattern structure. The
    *        term text is ignored.
    * @param automaton Automaton to run, terms that are accepted are considered a
diff --git a/lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java b/lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java
index f52df9fb9c..8509bcee07 100644
--- a/lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/BooleanQuery.java
@@ -52,6 +52,9 @@ public class BooleanQuery extends Query implements Iterable<BooleanClause> {
     public TooManyClauses() {
       super("maxClauseCount is set to " + maxClauseCount);
     }
+    public TooManyClauses(int maxClauseCount) {
+      super("maxClauseCount is set to " + maxClauseCount);
+    }
   }
 
   /** Return the maximum number of clauses permitted, 1024 by default.
diff --git a/lucene/core/src/java/org/apache/lucene/search/WildcardQuery.java b/lucene/core/src/java/org/apache/lucene/search/WildcardQuery.java
index b775dca40f..d943314810 100644
--- a/lucene/core/src/java/org/apache/lucene/search/WildcardQuery.java
+++ b/lucene/core/src/java/org/apache/lucene/search/WildcardQuery.java
@@ -71,12 +71,18 @@ public class WildcardQuery extends AutomatonQuery {
    * Convert Lucene wildcard syntax into an automaton.
    * @lucene.internal
    */
-  @SuppressWarnings("fallthrough")
   public static Automaton toAutomaton(Term wildcardquery) {
+    return toAutomaton(wildcardquery.text());
+  }
+
+  /**
+   * Convert Lucene wildcard syntax into an automaton.
+   * @lucene.internal
+   */
+  @SuppressWarnings("fallthrough")
+  public static Automaton toAutomaton(String wildcardText) {
     List<Automaton> automata = new ArrayList<>();
-    
-    String wildcardText = wildcardquery.text();
-    
+
     for (int i = 0; i < wildcardText.length();) {
       final int c = wildcardText.codePointAt(i);
       int length = Character.charCount(c);
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/AutomatonIntervalsSource.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/AutomatonIntervalsSource.java
new file mode 100644
index 0000000000..b9cacbeda6
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/AutomatonIntervalsSource.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.lucene.search.intervals;
+
+import java.io.IOException;
+
+import org.apache.lucene.index.Terms;
+import org.apache.lucene.index.TermsEnum;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.automaton.Automaton;
+import org.apache.lucene.util.automaton.CompiledAutomaton;
+import org.apache.lucene.util.automaton.Operations;
+
+/**
+ * An {@link MultiTermIntervalsSource} that matches terms matching an {@link Automaton}.
+ */
+public class AutomatonIntervalsSource extends MultiTermIntervalsSource {
+
+  /**
+   * the automaton to match index terms against
+   */
+  protected final Automaton automaton;
+  protected final CompiledAutomaton compiled;
+
+  /**
+   * possibly some pattern structure
+   */
+  protected final BytesRef term;
+  protected final boolean automatonIsBinary;
+
+  /**
+   * Create a new AutomatonIntervalsSource from an {@link Automaton}.
+   *
+   * @param term          Some pattern structure or null
+   * @param maxExpansions The max number of terms the automation can match.
+   * @param automaton     Automaton to run, terms that are accepted are considered a match.
+   */
+  public AutomatonIntervalsSource(final BytesRef term, final int maxExpansions, Automaton automaton) {
+    this(term, maxExpansions, automaton, Operations.DEFAULT_MAX_DETERMINIZED_STATES);
+  }
+
+  /**
+   * Create a new AutomatonIntervalsSource from an {@link Automaton}.
+   *
+   * @param term                  Some pattern structure or null
+   * @param maxExpansions         The max number of terms the automation can match.
+   * @param automaton             Automaton to run, terms that are accepted are considered a match.
+   * @param maxDeterminizedStates maximum number of states in the resulting
+   *                              automata.  If the automata would need more than this many states
+   *                              TooComplextToDeterminizeException is thrown.  Higher number require more
+   *                              space but can process more complex automata.
+   */
+  public AutomatonIntervalsSource(final BytesRef term, final int maxExpansions, Automaton automaton, int maxDeterminizedStates) {
+    this(term, maxExpansions, automaton, maxDeterminizedStates, false);
+  }
+
+  /**
+   * Create a new AutomatonIntervalsSource from an {@link Automaton}.
+   *
+   * @param term                  Some pattern structure or null
+   * @param maxExpansions         The max number of terms the automation can match.
+   * @param automaton             Automaton to run, terms that are accepted are considered a match.
+   * @param maxDeterminizedStates maximum number of states in the resulting
+   *                              automata.  If the automata would need more than this many states
+   *                              TooComplextToDeterminizeException is thrown.  Higher number require more
+   *                              space but can process more complex automata.
+   * @param isBinary              if true, this automaton is already binary and
+   *                              will not go through the UTF32ToUTF8 conversion
+   */
+  public AutomatonIntervalsSource(final BytesRef term, final int maxExpansions, Automaton automaton, int maxDeterminizedStates, boolean isBinary) {
+    super(maxExpansions);
+    this.term = term;
+    this.automaton = automaton;
+    this.automatonIsBinary = isBinary;
+    this.compiled = new CompiledAutomaton(automaton, null, true, maxDeterminizedStates, isBinary);
+  }
+
+  @Override
+  protected TermsEnum getTermsEnum(Terms terms) throws IOException {
+    return compiled.getTermsEnum(terms);
+  }
+
+  public BytesRef getTerm() {
+    return term;
+  }
+
+  @Override
+  public int hashCode() {
+    return 31 * super.hashCode() + compiled.hashCode() + (term == null ? 0 : term.hashCode());
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (getClass() != obj.getClass()) {
+      return false;
+    }
+    if (!super.equals(obj)) {
+      return false;
+    }
+
+    AutomatonIntervalsSource other = (AutomatonIntervalsSource) obj;
+    if (!compiled.equals(other.compiled)) {
+      return false;
+    }
+    if (term == null) {
+      if (other.term != null) {
+        return false;
+      }
+    } else if (!term.equals(other.term)) {
+      return false;
+    }
+
+    return true;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder buffer = new StringBuilder();
+    buffer.append(getClass().getSimpleName());
+    buffer.append(" {");
+    if (term != null) {
+      buffer.append("\nterm=")
+          .append(term.utf8ToString());
+    }
+    buffer.append('\n');
+    buffer.append(automaton.toString());
+    buffer.append("}");
+    return buffer.toString();
+  }
+}
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/DisjunctionIntervalIterator.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/DisjunctionIntervalIterator.java
new file mode 100644
index 0000000000..c688c2e2bd
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/DisjunctionIntervalIterator.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.lucene.search.intervals;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.apache.lucene.search.DocIdSetIterator;
+import org.apache.lucene.util.PriorityQueue;
+
+public class DisjunctionIntervalIterator extends IntervalIterator {
+
+  final DocIdSetIterator approximation;
+  final PriorityQueue<IntervalIterator> intervalQueue;
+  final DisiPriorityQueue disiQueue;
+  final List<IntervalIterator> iterators;
+  final float matchCost;
+
+  IntervalIterator current = EMPTY;
+
+  DisjunctionIntervalIterator(List<IntervalIterator> iterators) {
+    this.disiQueue = new DisiPriorityQueue(iterators.size());
+    for (IntervalIterator it : iterators) {
+      disiQueue.add(new DisiWrapper(it));
+    }
+    this.approximation = new DisjunctionDISIApproximation(disiQueue);
+    this.iterators = iterators;
+    this.intervalQueue = new PriorityQueue<IntervalIterator>(iterators.size()) {
+      @Override
+      protected boolean lessThan(IntervalIterator a, IntervalIterator b) {
+        return a.end() < b.end() || (a.end() == b.end() && a.start() >= b.start());
+      }
+    };
+    float costsum = 0;
+    for (IntervalIterator it : iterators) {
+      costsum += it.cost();
+    }
+    this.matchCost = costsum;
+  }
+
+  @Override
+  public float matchCost() {
+    return matchCost;
+  }
+
+  @Override
+  public int start() {
+    return current.start();
+  }
+
+  @Override
+  public int end() {
+    return current.end();
+  }
+
+  private void reset() throws IOException {
+    intervalQueue.clear();
+    for (DisiWrapper dw = disiQueue.topList(); dw != null; dw = dw.next) {
+      dw.intervals.nextInterval();
+      intervalQueue.add(dw.intervals);
+    }
+    current = EMPTY;
+  }
+
+  @Override
+  public int nextInterval() throws IOException {
+    if (current == EMPTY) {
+      if (intervalQueue.size() > 0) {
+        current = intervalQueue.top();
+      }
+      return current.start();
+    }
+    int start = current.start(), end = current.end();
+    while (intervalQueue.size() > 0 && contains(intervalQueue.top(), start, end)) {
+      IntervalIterator it = intervalQueue.pop();
+      if (it != null && it.nextInterval() != NO_MORE_INTERVALS) {
+        intervalQueue.add(it);
+      }
+    }
+    if (intervalQueue.size() == 0) {
+      current = EMPTY;
+      return NO_MORE_INTERVALS;
+    }
+    current = intervalQueue.top();
+    return current.start();
+  }
+
+  private boolean contains(IntervalIterator it, int start, int end) {
+    return start >= it.start() && start <= it.end() && end >= it.start() && end <= it.end();
+  }
+
+  @Override
+  public int docID() {
+    return approximation.docID();
+  }
+
+  @Override
+  public int nextDoc() throws IOException {
+    int doc = approximation.nextDoc();
+    reset();
+    return doc;
+  }
+
+  @Override
+  public int advance(int target) throws IOException {
+    int doc = approximation.advance(target);
+    reset();
+    return doc;
+  }
+
+  @Override
+  public long cost() {
+    return approximation.cost();
+  }
+}
\ No newline at end of file
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/DisjunctionIntervalsSource.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/DisjunctionIntervalsSource.java
index c7c7166d4b..90baa7a843 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/DisjunctionIntervalsSource.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/DisjunctionIntervalsSource.java
@@ -26,8 +26,6 @@ import java.util.stream.Collectors;
 
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.Term;
-import org.apache.lucene.search.DocIdSetIterator;
-import org.apache.lucene.util.PriorityQueue;
 
 class DisjunctionIntervalsSource extends IntervalsSource {
 
@@ -75,154 +73,4 @@ class DisjunctionIntervalsSource extends IntervalsSource {
       source.extractTerms(field, terms);
     }
   }
-
-  private static class DisjunctionIntervalIterator extends IntervalIterator {
-
-    final DocIdSetIterator approximation;
-    final PriorityQueue<IntervalIterator> intervalQueue;
-    final DisiPriorityQueue disiQueue;
-    final List<IntervalIterator> iterators;
-    final float matchCost;
-
-    IntervalIterator current = EMPTY;
-
-    DisjunctionIntervalIterator(List<IntervalIterator> iterators) {
-      this.disiQueue = new DisiPriorityQueue(iterators.size());
-      for (IntervalIterator it : iterators) {
-        disiQueue.add(new DisiWrapper(it));
-      }
-      this.approximation = new DisjunctionDISIApproximation(disiQueue);
-      this.iterators = iterators;
-      this.intervalQueue = new PriorityQueue<IntervalIterator>(iterators.size()) {
-        @Override
-        protected boolean lessThan(IntervalIterator a, IntervalIterator b) {
-          return a.end() < b.end() || (a.end() == b.end() && a.start() >= b.start());
-        }
-      };
-      float costsum = 0;
-      for (IntervalIterator it : iterators) {
-        costsum += it.cost();
-      }
-      this.matchCost = costsum;
-    }
-
-    @Override
-    public float matchCost() {
-      return matchCost;
-    }
-
-    @Override
-    public int start() {
-      return current.start();
-    }
-
-    @Override
-    public int end() {
-      return current.end();
-    }
-
-    private void reset() throws IOException {
-      intervalQueue.clear();
-      for (DisiWrapper dw = disiQueue.topList(); dw != null; dw = dw.next) {
-        dw.intervals.nextInterval();
-        intervalQueue.add(dw.intervals);
-      }
-      current = EMPTY;
-    }
-
-    @Override
-    public int nextInterval() throws IOException {
-      if (current == EMPTY) {
-        if (intervalQueue.size() > 0) {
-          current = intervalQueue.top();
-        }
-        return current.start();
-      }
-      int start = current.start(), end = current.end();
-      while (intervalQueue.size() > 0 && contains(intervalQueue.top(), start, end)) {
-        IntervalIterator it = intervalQueue.pop();
-        if (it != null && it.nextInterval() != NO_MORE_INTERVALS) {
-          intervalQueue.add(it);
-        }
-      }
-      if (intervalQueue.size() == 0) {
-        current = EMPTY;
-        return NO_MORE_INTERVALS;
-      }
-      current = intervalQueue.top();
-      return current.start();
-    }
-
-    private boolean contains(IntervalIterator it, int start, int end) {
-      return start >= it.start() && start <= it.end() && end >= it.start() && end <= it.end();
-    }
-
-    @Override
-    public int docID() {
-      return approximation.docID();
-    }
-
-    @Override
-    public int nextDoc() throws IOException {
-      int doc = approximation.nextDoc();
-      reset();
-      return doc;
-    }
-
-    @Override
-    public int advance(int target) throws IOException {
-      int doc = approximation.advance(target);
-      reset();
-      return doc;
-    }
-
-    @Override
-    public long cost() {
-      return approximation.cost();
-    }
-  }
-
-  private static final IntervalIterator EMPTY = new IntervalIterator() {
-
-    @Override
-    public int docID() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public int nextDoc() throws IOException {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public int advance(int target) throws IOException {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public long cost() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public int start() {
-      return -1;
-    }
-
-    @Override
-    public int end() {
-      return -1;
-    }
-
-    @Override
-    public int nextInterval() {
-      return NO_MORE_INTERVALS;
-    }
-
-    @Override
-    public float matchCost() {
-      return 0;
-    }
-  };
-
 }
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalFunction.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalFunction.java
index ddd891f61e..66e438f244 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalFunction.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalFunction.java
@@ -148,6 +148,10 @@ abstract class IntervalFunction {
         i = 1;
         if (subIterators.get(0).nextInterval() == IntervalIterator.NO_MORE_INTERVALS)
           return start;
+        if (start == -1) {
+          // handle start from an empty interval
+          return NO_MORE_INTERVALS;
+        }
       }
     }
 
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalIterator.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalIterator.java
index 242872001d..806bcb3158 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalIterator.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalIterator.java
@@ -74,4 +74,49 @@ public abstract class IntervalIterator extends DocIdSetIterator {
    */
   public abstract float matchCost();
 
-}
+  /**
+   * An empty interval
+   */
+  public static final IntervalIterator EMPTY = new IntervalIterator() {
+
+    @Override
+    public int docID() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int nextDoc() throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int advance(int target) throws IOException {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public long cost() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int start() {
+      return -1;
+    }
+
+    @Override
+    public int end() {
+      return -1;
+    }
+
+    @Override
+    public int nextInterval() {
+      return NO_MORE_INTERVALS;
+    }
+
+    @Override
+    public float matchCost() {
+      return 0;
+    }
+  };
+}
\ No newline at end of file
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalScorer.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalScorer.java
index 6672905df9..97fac325bb 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalScorer.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/IntervalScorer.java
@@ -48,16 +48,26 @@ class IntervalScorer extends Scorer {
   @Override
   public float score() throws IOException {
     ensureFreq();
+    if (simScorer == null) {
+      // no simScorer due to no collected terms or needsScores=false
+      // TODO: return constant score or freq?
+      return 1f;
+    }
+
     return simScorer.score(docID(), freq);
   }
 
   public Explanation explain(String topLevel) throws IOException {
     ensureFreq();
     Explanation freqExplanation = Explanation.match(freq, "intervalFreq=" + freq);
-    Explanation scoreExplanation = simScorer.explain(docID(), freqExplanation);
-    return Explanation.match(scoreExplanation.getValue(),
-        topLevel + ", result of:",
-        scoreExplanation);
+    Explanation scoreExplanation;
+    if (simScorer == null) {
+      scoreExplanation = Explanation.match(1f, "null simScorer");
+    } else {
+      scoreExplanation = simScorer.explain(docID(), freqExplanation);
+    }
+
+    return Explanation.match(scoreExplanation.getValue(), topLevel + ", result of:", scoreExplanation);
   }
 
   public float freq() throws IOException {
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/Intervals.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/Intervals.java
index b360919296..21ced153ae 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/Intervals.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/Intervals.java
@@ -19,18 +19,20 @@ package org.apache.lucene.search.intervals;
 
 import java.util.Arrays;
 
+import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.util.BytesRef;
 
 /**
  * Constructor functions for {@link IntervalsSource} types
- *
+ * <p>
  * These sources implement minimum-interval algorithms taken from the paper
  * <a href="http://vigna.di.unimi.it/ftp/papers/EfficientAlgorithmsMinimalIntervalSemantics.pdf">
  * Efficient Optimally Lazy Algorithms for Minimal-Interval Semantics</a>
  */
 public final class Intervals {
 
-  private Intervals() {}
+  private Intervals() {
+  }
 
   /**
    * Return an {@link IntervalsSource} exposing intervals for a term
@@ -66,6 +68,100 @@ public final class Intervals {
     return new ConjunctionIntervalsSource(Arrays.asList(subSources), IntervalFunction.BLOCK);
   }
 
+  /**
+   * Return an {@link IntervalsSource} exposing intervals for terms matching a wildcard pattern.
+   *
+   * @param wildcard the wildcard pattern
+   */
+  public static IntervalsSource wildcard(BytesRef wildcard) {
+    return new WildcardIntervalsSource(wildcard, BooleanQuery.getMaxClauseCount());
+  }
+
+  /**
+   * Return an {@link IntervalsSource} exposing intervals for terms matching a wildcard pattern.
+   *
+   * @param maxExpansions the max number of terms the wildcard can expand to
+   * @param wildcard      the wildcard pattern
+   */
+  public static IntervalsSource wildcard(BytesRef wildcard, int maxExpansions) {
+    return new WildcardIntervalsSource(wildcard, maxExpansions);
+  }
+
+  /**
+   * Return an {@link IntervalsSource} exposing intervals for terms matching a wildcard pattern.
+   *
+   * @param wildcard the wildcard pattern
+   */
+  public static IntervalsSource wildcard(String wildcard) {
+    return new WildcardIntervalsSource(wildcard, BooleanQuery.getMaxClauseCount());
+  }
+
+  /**
+   * Return an {@link IntervalsSource} exposing intervals for terms matching a wildcard pattern.
+   *
+   * @param maxExpansions the max number of terms the wildcard can expand to
+   * @param wildcard      the wildcard pattern
+   */
+  public static IntervalsSource wildcard(String wildcard, int maxExpansions) {
+    return new WildcardIntervalsSource(wildcard, maxExpansions);
+  }
+
+  /**
+   * Return an {@link IntervalsSource} exposing intervals for terms with the given prefix.
+   */
+  public static IntervalsSource prefix(BytesRef prefix) {
+    return new PrefixIntervalsSource(prefix, BooleanQuery.getMaxClauseCount());
+  }
+
+  /**
+   * Return an {@link IntervalsSource} exposing intervals for terms with the given prefix.
+   */
+  public static IntervalsSource prefix(BytesRef prefix, int maxExpansions) {
+    return new PrefixIntervalsSource(prefix, maxExpansions);
+  }
+
+  /**
+   * Return an {@link IntervalsSource} exposing intervals for terms with the given prefix.
+   */
+  public static IntervalsSource prefix(String prefix) {
+    return prefix(new BytesRef(prefix));
+  }
+
+  /**
+   * Return an {@link IntervalsSource} exposing intervals for terms with the given prefix.
+   */
+  public static IntervalsSource prefix(String prefix, int maxExpansions) {
+    return prefix(new BytesRef(prefix), maxExpansions);
+  }
+
+  /**
+   * Return an {@link IntervalsSource} exposing intervals for terms matching a regular expression.
+   */
+  public static IntervalsSource regexp(BytesRef pattern) {
+    return new RegexpIntervalsSource(pattern, BooleanQuery.getMaxClauseCount());
+  }
+
+  /**
+   * Return an {@link IntervalsSource} exposing intervals for terms matching a regular expression.
+   */
+  public static IntervalsSource regexp(BytesRef pattern, int maxExpansions) {
+    return new RegexpIntervalsSource(pattern, maxExpansions);
+  }
+
+  /**
+   * Return an {@link IntervalsSource} exposing intervals for terms matching a regular expression.
+   */
+  public static IntervalsSource regexp(String pattern) {
+    return regexp(new BytesRef(pattern));
+  }
+
+  /**
+   * Return an {@link IntervalsSource} exposing intervals for terms matching a regular expression.
+   */
+  public static IntervalsSource regexp(String pattern, int maxExpansions) {
+    return regexp(new BytesRef(pattern), maxExpansions);
+  }
+
   /**
    * Return an {@link IntervalsSource} over the disjunction of a set of sub-sources
    */
@@ -77,8 +173,9 @@ public final class Intervals {
 
   /**
    * Create an {@link IntervalsSource} that filters a sub-source by the width of its intervals
-   * @param width       the maximum width of intervals in the sub-source ot return
-   * @param subSource   the sub-source to filter
+   *
+   * @param width     the maximum width of intervals in the sub-source ot return
+   * @param subSource the sub-source to filter
    */
   public static IntervalsSource maxwidth(int width, IntervalsSource subSource) {
     return new LowpassIntervalsSource(subSource, width);
@@ -86,10 +183,10 @@ public final class Intervals {
 
   /**
    * Create an ordered {@link IntervalsSource} with an unbounded width range
-   *
+   * <p>
    * Returns intervals in which the subsources all appear in the given order
    *
-   * @param subSources  an ordered set of {@link IntervalsSource} objects
+   * @param subSources an ordered set of {@link IntervalsSource} objects
    */
   public static IntervalsSource ordered(IntervalsSource... subSources) {
     return new ConjunctionIntervalsSource(Arrays.asList(subSources), IntervalFunction.ORDERED);
@@ -97,10 +194,10 @@ public final class Intervals {
 
   /**
    * Create an unordered {@link IntervalsSource} with an unbounded width range
-   *
+   * <p>
    * Returns intervals in which all the subsources appear.
    *
-   * @param subSources  an unordered set of queries
+   * @param subSources an unordered set of queries
    */
   public static IntervalsSource unordered(IntervalsSource... subSources) {
     return new ConjunctionIntervalsSource(Arrays.asList(subSources), IntervalFunction.UNORDERED);
@@ -108,11 +205,11 @@ public final class Intervals {
 
   /**
    * Create a non-overlapping IntervalsSource
-   *
+   * <p>
    * Returns intervals of the minuend that do not overlap with intervals from the subtrahend
-
-   * @param minuend     the {@link IntervalsSource} to filter
-   * @param subtrahend  the {@link IntervalsSource} to filter by
+   *
+   * @param minuend    the {@link IntervalsSource} to filter
+   * @param subtrahend the {@link IntervalsSource} to filter by
    */
   public static IntervalsSource nonOverlapping(IntervalsSource minuend, IntervalsSource subtrahend) {
     return new DifferenceIntervalsSource(minuend, subtrahend, DifferenceIntervalFunction.NON_OVERLAPPING);
@@ -120,14 +217,14 @@ public final class Intervals {
 
   /**
    * Create a not-within {@link IntervalsSource}
-   *
+   * <p>
    * Returns intervals of the minuend that do not appear within a set number of positions of
    * intervals from the subtrahend query
    *
-   * @param minuend     the {@link IntervalsSource} to filter
-   * @param positions   the maximum distance that intervals from the minuend may occur from intervals
-   *                    of the subtrahend
-   * @param subtrahend  the {@link IntervalsSource} to filter by
+   * @param minuend    the {@link IntervalsSource} to filter
+   * @param positions  the maximum distance that intervals from the minuend may occur from intervals
+   *                   of the subtrahend
+   * @param subtrahend the {@link IntervalsSource} to filter by
    */
   public static IntervalsSource notWithin(IntervalsSource minuend, int positions, IntervalsSource subtrahend) {
     return new DifferenceIntervalsSource(minuend, subtrahend, new DifferenceIntervalFunction.NotWithinFunction(positions));
@@ -135,11 +232,11 @@ public final class Intervals {
 
   /**
    * Create a not-containing {@link IntervalsSource}
-   *
+   * <p>
    * Returns intervals from the minuend that do not contain intervals of the subtrahend
    *
-   * @param minuend     the {@link IntervalsSource} to filter
-   * @param subtrahend  the {@link IntervalsSource} to filter by
+   * @param minuend    the {@link IntervalsSource} to filter
+   * @param subtrahend the {@link IntervalsSource} to filter by
    */
   public static IntervalsSource notContaining(IntervalsSource minuend, IntervalsSource subtrahend) {
     return new DifferenceIntervalsSource(minuend, subtrahend, DifferenceIntervalFunction.NOT_CONTAINING);
@@ -147,12 +244,12 @@ public final class Intervals {
 
   /**
    * Create a containing {@link IntervalsSource}
-   *
+   * <p>
    * Returns intervals from the big source that contain one or more intervals from
    * the small source
    *
-   * @param big     the {@link IntervalsSource} to filter
-   * @param small   the {@link IntervalsSource} to filter by
+   * @param big   the {@link IntervalsSource} to filter
+   * @param small the {@link IntervalsSource} to filter by
    */
   public static IntervalsSource containing(IntervalsSource big, IntervalsSource small) {
     return new ConjunctionIntervalsSource(Arrays.asList(big, small), IntervalFunction.CONTAINING);
@@ -160,12 +257,12 @@ public final class Intervals {
 
   /**
    * Create a not-contained-by {@link IntervalsSource}
-   *
+   * <p>
    * Returns intervals from the small {@link IntervalsSource} that do not appear within
    * intervals from the big {@link IntervalsSource}.
    *
-   * @param small   the {@link IntervalsSource} to filter
-   * @param big     the {@link IntervalsSource} to filter by
+   * @param small the {@link IntervalsSource} to filter
+   * @param big   the {@link IntervalsSource} to filter by
    */
   public static IntervalsSource notContainedBy(IntervalsSource small, IntervalsSource big) {
     return new DifferenceIntervalsSource(small, big, DifferenceIntervalFunction.NOT_CONTAINED_BY);
@@ -173,11 +270,11 @@ public final class Intervals {
 
   /**
    * Create a contained-by {@link IntervalsSource}
-   *
+   * <p>
    * Returns intervals from the small query that appear within intervals of the big query
    *
-   * @param small     the {@link IntervalsSource} to filter
-   * @param big       the {@link IntervalsSource} to filter by
+   * @param small the {@link IntervalsSource} to filter
+   * @param big   the {@link IntervalsSource} to filter by
    */
   public static IntervalsSource containedBy(IntervalsSource small, IntervalsSource big) {
     return new ConjunctionIntervalsSource(Arrays.asList(small, big), IntervalFunction.CONTAINED_BY);
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/MultiTermIntervalsSource.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/MultiTermIntervalsSource.java
new file mode 100644
index 0000000000..0b046d8aa8
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/MultiTermIntervalsSource.java
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.lucene.search.intervals;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import org.apache.lucene.index.LeafReaderContext;
+import org.apache.lucene.index.PostingsEnum;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.index.Terms;
+import org.apache.lucene.index.TermsEnum;
+import org.apache.lucene.search.BooleanQuery;
+
+/**
+ * An {@link IntervalsSource} that matches intervals from a {@link TermsEnum}.  This source does not extract any terms
+ * for scoring since terms are gathered per-segment.
+ */
+public abstract class MultiTermIntervalsSource extends IntervalsSource {
+
+  protected final int maxExpansions;
+  protected boolean errorOnMaxExpansions = false;
+
+  /**
+   * Creates the multi-terms intervals source and sets the max term expansions {@code maxExpansions}.  There is a hard
+   * limit of {@link BooleanQuery#getMaxClauseCount()}.
+   *
+   * @param maxExpansions value >= 1
+   * @throws IllegalArgumentException when {@code maxExpansions} < 1
+   */
+  public MultiTermIntervalsSource(int maxExpansions) {
+    if (maxExpansions >= 1) {
+      // BooleanQuery#getMaxClauseCount() is a hard limit
+      this.maxExpansions = Math.min(maxExpansions, BooleanQuery.getMaxClauseCount());
+    } else {
+      throw new IllegalArgumentException("maxExpansions must be >= 1");
+    }
+  }
+
+  @Override
+  public IntervalIterator intervals(String field, LeafReaderContext ctx) throws IOException {
+    Terms terms = ctx.reader().terms(field);
+    if (terms == null) {
+      return null;
+    }
+
+    if (!terms.hasPositions()) {
+      throw new IllegalArgumentException("Cannot create an IntervalIterator over field " + field + " because it has no indexed positions");
+    }
+
+    TermsEnum termsEnum = getTermsEnum(terms);
+    assert termsEnum != null;
+
+    List<IntervalIterator> subIterators = new ArrayList<>();
+
+    // create an interval for every term matching the termsEnum, limited to maxExpansions subIterators
+    while (termsEnum.next() != null) {
+      if (subIterators.size() >= maxExpansions) {
+        if (errorOnMaxExpansions) {
+          throw new BooleanQuery.TooManyClauses(maxExpansions);
+        }
+
+        break;
+      }
+
+      final PostingsEnum pe = termsEnum.postings(null, PostingsEnum.POSITIONS);
+      subIterators.add(new IntervalIterator() {
+        @Override
+        public int docID() {
+          return pe.docID();
+        }
+
+        @Override
+        public int nextDoc() throws IOException {
+          int doc = pe.nextDoc();
+          reset();
+          return doc;
+        }
+
+        @Override
+        public int advance(int target) throws IOException {
+          int doc = pe.advance(target);
+          reset();
+          return doc;
+        }
+
+        @Override
+        public long cost() {
+          return pe.cost();
+        }
+
+        int pos = -1, upto;
+
+        @Override
+        public int start() {
+          return pos;
+        }
+
+        @Override
+        public int end() {
+          return pos;
+        }
+
+        @Override
+        public int nextInterval() throws IOException {
+          if (upto <= 0)
+            return pos = NO_MORE_INTERVALS;
+          upto--;
+          return pos = pe.nextPosition();
+        }
+
+        @Override
+        public float matchCost() {
+          return 0;
+        }
+
+        private void reset() throws IOException {
+          if (pe.docID() == NO_MORE_DOCS) {
+            upto = -1;
+            pos = NO_MORE_INTERVALS;
+          } else {
+            upto = pe.freq();
+            pos = -1;
+          }
+        }
+      });
+    }
+
+    if (subIterators.isEmpty()) {
+      return null;
+    } else if (subIterators.size() == 1) {
+      return subIterators.get(0);
+    } else {
+      return new DisjunctionIntervalIterator(subIterators);
+    }
+  }
+
+  /**
+   * Construct the enumeration to be used, expanding the
+   * pattern term.  This method should not return null
+   * (should instead return {@link TermsEnum#EMPTY} if no
+   * terms match).  The TermsEnum must already be
+   * positioned to the first matching term.
+   */
+  protected abstract TermsEnum getTermsEnum(Terms terms) throws IOException;
+
+  @Override
+  public void extractTerms(String field, Set<Term> terms) {
+    // no terms known since they are collected on each segment
+  }
+
+  /**
+   * If a {@link BooleanQuery.TooManyClauses} exception will be thrown when {@link #maxExpansions} is hit.
+   */
+  public boolean isErrorOnMaxExpansions() {
+    return errorOnMaxExpansions;
+  }
+
+  /**
+   * Sets if reaching {@link #maxExpansions} will result in an {@link BooleanQuery.TooManyClauses} exception or just
+   * stops term expansion.
+   */
+  public void setErrorOnMaxExpansions(boolean errorOnMaxExpansions) {
+    this.errorOnMaxExpansions = errorOnMaxExpansions;
+  }
+
+  @Override
+  public int hashCode() {
+    return 31 * Objects.hash(maxExpansions, errorOnMaxExpansions);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+    if (other == null || getClass() != other.getClass()) {
+      return false;
+    }
+
+    MultiTermIntervalsSource that = (MultiTermIntervalsSource) other;
+    return Objects.equals(maxExpansions, that.maxExpansions)
+        && Objects.equals(errorOnMaxExpansions, that.errorOnMaxExpansions);
+  }
+}
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/PrefixIntervalsSource.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/PrefixIntervalsSource.java
new file mode 100644
index 0000000000..0b0740e3f9
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/PrefixIntervalsSource.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.lucene.search.intervals;
+
+import org.apache.lucene.search.PrefixQuery;
+import org.apache.lucene.util.BytesRef;
+
+/**
+ * An {@link AutomatonIntervalsSource} that uses an automaton accepting all terms with the specified prefix.
+ */
+public class PrefixIntervalsSource extends AutomatonIntervalsSource {
+  public PrefixIntervalsSource(BytesRef prefix, int maxExpansions) {
+    super(prefix, maxExpansions, PrefixQuery.toAutomaton(prefix));
+  }
+
+  @Override
+  public String toString() {
+    return "prefix(" + term.utf8ToString() + ")";
+  }
+}
\ No newline at end of file
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/RegexpIntervalsSource.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/RegexpIntervalsSource.java
new file mode 100644
index 0000000000..b42e6ae3c1
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/RegexpIntervalsSource.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.lucene.search.intervals;
+
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.automaton.AutomatonProvider;
+import org.apache.lucene.util.automaton.Operations;
+import org.apache.lucene.util.automaton.RegExp;
+
+/**
+ * An {@link AutomatonIntervalsSource} that uses an automaton accepting all terms that match the specified regular
+ * expression.
+ */
+public class RegexpIntervalsSource extends AutomatonIntervalsSource {
+  /**
+   * A provider that provides no named automata
+   */
+  private static AutomatonProvider defaultProvider = name -> null;
+
+  public RegexpIntervalsSource(BytesRef pattern, int maxExpansions) {
+    this(pattern, RegExp.ALL, maxExpansions);
+  }
+
+  public RegexpIntervalsSource(BytesRef pattern, int flags, int maxExpansions) {
+    this(pattern, flags, maxExpansions, Operations.DEFAULT_MAX_DETERMINIZED_STATES);
+  }
+
+  public RegexpIntervalsSource(BytesRef pattern, int flags, int maxExpansions, int maxDeterminizedStates) {
+    this(pattern, flags, maxExpansions, defaultProvider, maxDeterminizedStates);
+  }
+
+  public RegexpIntervalsSource(BytesRef pattern, int flags, int maxExpansions, AutomatonProvider provider, int maxDeterminizedStates) {
+    super(pattern, maxExpansions,
+        new RegExp(pattern.utf8ToString(), flags).toAutomaton(provider, maxDeterminizedStates), maxDeterminizedStates);
+  }
+
+  @Override
+  public String toString() {
+    return "regexp(" + term.utf8ToString() + ")";
+  }
+}
\ No newline at end of file
diff --git a/lucene/sandbox/src/java/org/apache/lucene/search/intervals/WildcardIntervalsSource.java b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/WildcardIntervalsSource.java
new file mode 100644
index 0000000000..bf4c91ab03
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/search/intervals/WildcardIntervalsSource.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.lucene.search.intervals;
+
+import org.apache.lucene.search.WildcardQuery;
+import org.apache.lucene.util.BytesRef;
+
+/**
+ * An {@link AutomatonIntervalsSource} that uses an automaton accepting all terms matching the wildcard pattern.
+ */
+public class WildcardIntervalsSource extends AutomatonIntervalsSource {
+
+  public WildcardIntervalsSource(BytesRef term, int maxExpansions) {
+    super(term, maxExpansions, WildcardQuery.toAutomaton(term.utf8ToString()));
+  }
+
+  public WildcardIntervalsSource(BytesRef term, int maxExpansions, int maxDeterminizedStates) {
+    super(term, maxExpansions, WildcardQuery.toAutomaton(term.utf8ToString()), maxDeterminizedStates);
+  }
+
+  public WildcardIntervalsSource(String term, int maxExpansions) {
+    super(new BytesRef(term), maxExpansions, WildcardQuery.toAutomaton(term));
+  }
+
+  public WildcardIntervalsSource(String term, int maxExpansions, int maxDeterminizedStates) {
+    super(new BytesRef(term), maxExpansions, WildcardQuery.toAutomaton(term), maxDeterminizedStates);
+  }
+
+  @Override
+  public String toString() {
+    return "wildcard(" + term.utf8ToString() + ")";
+  }
+}
diff --git a/lucene/sandbox/src/test/org/apache/lucene/search/intervals/TestIntervalQuery.java b/lucene/sandbox/src/test/org/apache/lucene/search/intervals/TestIntervalQuery.java
index 8f0623e836..2761c4dc4c 100644
--- a/lucene/sandbox/src/test/org/apache/lucene/search/intervals/TestIntervalQuery.java
+++ b/lucene/sandbox/src/test/org/apache/lucene/search/intervals/TestIntervalQuery.java
@@ -81,6 +81,51 @@ public class TestIntervalQuery extends LuceneTestCase {
         new int[]{0});
   }
 
+  public void testWildcardQuery() throws IOException {
+    Query q = new IntervalQuery(field, Intervals.ordered(Intervals.wildcard("ge*"), Intervals.wildcard("*search")));
+    checkHits(q, new int[]{6, 7});
+  }
+
+  public void testWildcardQueryWidth2() throws IOException {
+    Query q = new IntervalQuery(field, Intervals.maxwidth(2,
+        Intervals.ordered(Intervals.wildcard("ge*"), Intervals.wildcard("*search"))));
+    checkHits(q, new int[]{7});
+  }
+
+  public void testMultiWildcard() throws IOException {
+    Query q = new IntervalQuery(field, Intervals.ordered(Intervals.wildcard("w*"), Intervals.term("w4")));
+    checkHits(q, new int[]{0, 2, 5});
+  }
+
+  public void testPrefixQuery() throws IOException {
+    Query q = new IntervalQuery(field, Intervals.ordered(Intervals.prefix("w"), Intervals.term("w4")));
+    checkHits(q, new int[]{0, 2, 5});
+  }
+
+  public void testMultiTermPhrase() throws IOException {
+    Query q = new IntervalQuery(field, Intervals.phrase(Intervals.prefix("w"), Intervals.regexp("w[048]")));
+    checkHits(q, new int[]{0, 2, 5});
+  }
+
+  public void testRegexpQuery() throws IOException {
+    Query q = new IntervalQuery(field, Intervals.ordered(Intervals.regexp("w[13]"),
+        Intervals.regexp("w4|xx|yy")));
+    checkHits(q, new int[]{0, 2, 3, 5});
+  }
+
+  public void testRegexpQueryWidth2() throws IOException {
+    Query q = new IntervalQuery(field, Intervals.maxwidth(2,
+        Intervals.ordered(Intervals.regexp("w[13]"), Intervals.regexp("w4|xx|yy"))));
+    checkHits(q, new int[]{0, 2, 3});
+  }
+
+  public void testOrderedWithMultiClauseOrQuery() throws IOException {
+    Query q = new IntervalQuery(field, Intervals.ordered(
+        Intervals.or(Intervals.term("w1"), Intervals.term("w3")),
+        Intervals.or(Intervals.term("w4"), Intervals.term("xx"), Intervals.term("yy"))));
+    checkHits(q, new int[]{0, 2, 3, 5});
+  }
+
   public void testOrderedNearQueryWidth3() throws IOException {
     checkHits(new IntervalQuery(field, Intervals.maxwidth(3, Intervals.ordered(Intervals.term("w1"), Intervals.term("w2")))),
         new int[]{0, 1, 2, 5});
diff --git a/lucene/sandbox/src/test/org/apache/lucene/search/intervals/TestIntervals.java b/lucene/sandbox/src/test/org/apache/lucene/search/intervals/TestIntervals.java
index 9405f79e5f..21ec88f95c 100644
--- a/lucene/sandbox/src/test/org/apache/lucene/search/intervals/TestIntervals.java
+++ b/lucene/sandbox/src/test/org/apache/lucene/search/intervals/TestIntervals.java
@@ -32,8 +32,10 @@ import org.apache.lucene.index.DocValues;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.NumericDocValues;
 import org.apache.lucene.index.RandomIndexWriter;
+import org.apache.lucene.search.BooleanQuery;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.store.Directory;
+import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.IOUtils;
 import org.apache.lucene.util.LuceneTestCase;
 import org.junit.AfterClass;
@@ -137,6 +139,79 @@ public class TestIntervals extends LuceneTestCase {
     });
   }
 
+  public void testMultiTermQueryIntervals() throws IOException {
+    checkIntervals(Intervals.prefix("pe"), "field1", 4, new int[][]{
+        {},
+        { 0, 0, 3, 3, 6, 6 },
+        { 0, 0, 3, 3, 6, 6 },
+        { 7, 7 },
+        { 0, 0, 3, 3, 6, 6 },
+        {}
+    });
+  }
+
+  public void testPhraseMultiTermQueryIntervals() throws IOException {
+    checkIntervals(Intervals.phrase(Intervals.prefix("pe"), Intervals.prefix("po")), "field1", 3, new int[][]{
+        {},
+        { 0, 1, 3, 4, 6, 7 },
+        { 0, 1, 3, 4, 6, 7 },
+        {},
+        { 0, 1, 3, 4, 6, 7 },
+        {}
+    });
+  }
+
+  public void testMultiTermWithMaxExpansions() throws IOException {
+    BooleanQuery.TooManyClauses e = expectThrows(BooleanQuery.TooManyClauses.class, () -> {
+      PrefixIntervalsSource source = (PrefixIntervalsSource) Intervals.prefix("p", 1);
+      source.setErrorOnMaxExpansions(true);
+      checkIntervals(source, "field1", 4,
+          new int[][]{{}});
+    });
+
+    assertEquals("maxClauseCount is set to 1", e.getMessage());
+  }
+
+  public void testMultiTermWithMaxExpansionsNoError() throws IOException {
+    // only 3 terms starting with "no":  Nothing, Nor, nowt
+    // should collect only "Nor" since that is first in index-order
+    checkIntervals(Intervals.prefix("no", 1), "field1", 1, new int[][]{
+        {},
+        {},
+        {},
+        { 0, 0 },
+        {},
+        {}
+    });
+  }
+
+  public void testMultiTermWithHittingHardMaxExpansions() throws IOException {
+    int originalHardMax = BooleanQuery.getMaxClauseCount();
+    BooleanQuery.setMaxClauseCount(1);
+    try {
+      BooleanQuery.TooManyClauses e = expectThrows(BooleanQuery.TooManyClauses.class, () -> {
+        PrefixIntervalsSource source = (PrefixIntervalsSource) Intervals.prefix("p", Integer.MAX_VALUE);
+        source.setErrorOnMaxExpansions(true);
+
+        checkIntervals(source, "field1", 4,
+            new int[][]{{}});
+      });
+
+      assertEquals("maxClauseCount is set to 1", e.getMessage());
+    } finally {
+      BooleanQuery.setMaxClauseCount(originalHardMax);
+    }
+  }
+
+  public void testMultiTermWithInvalidMaxExpansions() throws IOException {
+    IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> {
+      checkIntervals(Intervals.prefix("p", -1), "field1", 4,
+          new int[][]{{}});
+    });
+
+    assertEquals("maxExpansions must be >= 1", e.getMessage());
+  }
+
   public void testOrderedNearIntervals() throws IOException {
     checkIntervals(Intervals.ordered(Intervals.term("pease"), Intervals.term("hot")),
         "field1", 3, new int[][]{
