diff --git a/src/main/kotlin/org/acejump/boundaries/EditorOffsetCache.kt b/src/main/kotlin/org/acejump/boundaries/EditorOffsetCache.kt
index ecb3e3b..be2a9c2 100644
--- a/src/main/kotlin/org/acejump/boundaries/EditorOffsetCache.kt
+++ b/src/main/kotlin/org/acejump/boundaries/EditorOffsetCache.kt
@@ -21,6 +21,11 @@ sealed class EditorOffsetCache {
    */
   abstract fun visibleArea(editor: Editor): Pair<Point, Point>
 
+  /**
+   * Returns whether the offset is in the visible area rectangle.
+   */
+  abstract fun isVisible(editor: Editor, offset: Int): Boolean
+  
   /**
    * Returns the editor offset at the provided pixel coordinate.
    */
@@ -35,6 +40,7 @@ sealed class EditorOffsetCache {
 
   private class Cache: EditorOffsetCache() {
     private var visibleArea: Pair<Point, Point>? = null
+    private val lineToVisibleOffsetRange = Int2ObjectOpenHashMap<IntRange>()
     private val pointToOffset =
       Object2IntOpenHashMap<Point>().apply { defaultReturnValue(-1) }
     private val offsetToPoint = Int2ObjectOpenHashMap<Point>()
@@ -42,6 +48,24 @@ sealed class EditorOffsetCache {
     override fun visibleArea(editor: Editor): Pair<Point, Point> =
       visibleArea ?: Uncached.visibleArea(editor).also { visibleArea = it }
 
+    override fun isVisible(editor: Editor, offset: Int): Boolean {
+      val visualLine = editor.offsetToVisualLine(offset, false)
+
+      var visibleRange = lineToVisibleOffsetRange.get(visualLine)
+      if (visibleRange == null) {
+        val (topLeft, bottomRight) = visibleArea(editor)
+        val lineY = editor.visualLineToY(visualLine)
+
+        val firstVisibleOffset = xyToOffset(editor, Point(topLeft.x, lineY))
+        val lastVisibleOffset = xyToOffset(editor, Point(bottomRight.x, lineY))
+
+        visibleRange = firstVisibleOffset..lastVisibleOffset
+        lineToVisibleOffsetRange.put(visualLine, visibleRange)
+      }
+
+      return offset in visibleRange
+    }
+
     override fun xyToOffset(editor: Editor, pos: Point): Int =
       pointToOffset.getInt(pos).let { offset ->
         if (offset != -1) offset
@@ -64,6 +88,15 @@ sealed class EditorOffsetCache {
         )
       }
 
+    override fun isVisible(editor: Editor, offset: Int): Boolean {
+      val (topLeft, bottomRight) = visibleArea(editor)
+      val pos = offsetToXY(editor, offset)
+      val x = pos.x
+      val y = pos.y
+
+      return x >= topLeft.x && y >= topLeft.y && x <= bottomRight.x && y <= bottomRight.y
+    }
+
     override fun xyToOffset(editor: Editor, pos: Point): Int =
       read { editor.logicalPositionToOffset(editor.xyToLogicalPosition(pos)) }
 
diff --git a/src/main/kotlin/org/acejump/boundaries/StandardBoundaries.kt b/src/main/kotlin/org/acejump/boundaries/StandardBoundaries.kt
index 1a47935..da034e1 100644
--- a/src/main/kotlin/org/acejump/boundaries/StandardBoundaries.kt
+++ b/src/main/kotlin/org/acejump/boundaries/StandardBoundaries.kt
@@ -21,22 +21,7 @@ enum class StandardBoundaries : Boundaries {
     }
     
     override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
-      // If we are not using a cache, calling getOffsetRange will cause
-      // additional 1-2 pixel coordinate -> offset lookups, which is a lot
-      // more expensive than one lookup compared against the visible area.
-      
-      // However, if we are using a cache, it's likely that the topmost and
-      // bottommost positions are already cached whereas the provided offset
-      // isn't, so we save a lookup for every offset outside the range.
-      
-      if (cache !== EditorOffsetCache.Uncached && offset !in getOffsetRange(editor, cache)) return false
-      
-      val (topLeft, bottomRight) = cache.visibleArea(editor)
-      val pos = cache.offsetToXY(editor, offset)
-      val x = pos.x
-      val y = pos.y
-      
-      return x >= topLeft.x && y >= topLeft.y && x <= bottomRight.x && y <= bottomRight.y
+      return cache.isVisible(editor, offset)
     }
   },
   
diff --git a/src/main/kotlin/org/acejump/search/SearchProcessor.kt b/src/main/kotlin/org/acejump/search/SearchProcessor.kt
index bc9cfca..c7fdd56 100644
--- a/src/main/kotlin/org/acejump/search/SearchProcessor.kt
+++ b/src/main/kotlin/org/acejump/search/SearchProcessor.kt
@@ -3,6 +3,7 @@ package org.acejump.search
 import com.intellij.openapi.editor.Editor
 import it.unimi.dsi.fastutil.ints.IntArrayList
 import org.acejump.boundaries.Boundaries
+import org.acejump.boundaries.EditorOffsetCache
 import org.acejump.immutableText
 import org.acejump.isWordPart
 import org.acejump.matchesAt
@@ -12,7 +13,6 @@ import org.acejump.matchesAt
  * previous results when the user [type]s a character.
  */
 internal class SearchProcessor private constructor(
-  private val editors: List<Editor>,
   query: SearchQuery,
   results: MutableMap<Editor, IntArrayList>
 ) {
@@ -24,14 +24,15 @@ internal class SearchProcessor private constructor(
       SearchProcessor(editors, SearchQuery.RegularExpression(pattern), boundaries)
   }
   
-  private constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries) : this(editors, query, mutableMapOf()) {
+  private constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries) : this(query, mutableMapOf()) {
     val regex = query.toRegex()
     
     if (regex != null) {
       for (editor in editors) {
+        val cache = EditorOffsetCache.new()
         val offsets = IntArrayList()
         
-        val offsetRange = boundaries.getOffsetRange(editor)
+        val offsetRange = boundaries.getOffsetRange(editor, cache)
         var result = regex.find(editor.immutableText, offsetRange.first)
         
         while (result != null) {
@@ -43,7 +44,7 @@ internal class SearchProcessor private constructor(
           if (highlightEnd > offsetRange.last) {
             break
           }
-          else if (boundaries.isOffsetInside(editor, index)) {
+          else if (boundaries.isOffsetInside(editor, index, cache)) {
             offsets.add(index)
           }