diff --git a/examples/pdf/multipage/viewer.qml b/examples/pdf/multipage/viewer.qml
index 3d9cd371ad86ad531c47157a43bea97117e32777..8f102a3c112d25af5792ae67999aa6e345588c5b 100644
--- a/examples/pdf/multipage/viewer.qml
+++ b/examples/pdf/multipage/viewer.qml
@@ -230,6 +230,7 @@ ApplicationWindow {
     PdfMultiPageView {
         id: view
         anchors.fill: parent
+        anchors.leftMargin: searchDrawer.position * searchDrawer.width
         document: root.document
         searchString: searchField.text
         onCurrentPageChanged: currentPageSB.value = view.currentPage + 1
@@ -237,10 +238,11 @@ ApplicationWindow {
 
     Drawer {
         id: searchDrawer
-        edge: Qt.BottomEdge
-        x: 20
+        edge: Qt.LeftEdge
+        modal: false
         width: searchLayout.implicitWidth
-        height: searchLayout.implicitHeight
+        y: root.header.height
+        height: view.height
         dim: false
         Shortcut {
             sequence: StandardKey.Find
@@ -249,45 +251,69 @@ ApplicationWindow {
                 searchField.forceActiveFocus()
             }
         }
-        RowLayout {
+        ColumnLayout {
             id: searchLayout
-            ToolButton {
-                action: Action {
-                    icon.source: "resources/go-up-search.svg"
-                    onTriggered: view.searchBack()
+            anchors.fill: parent
+            anchors.margins: 2
+            RowLayout {
+                ToolButton {
+                    action: Action {
+                        icon.source: "resources/go-up-search.svg"
+                        shortcut: StandardKey.FindPrevious
+                        onTriggered: view.searchBack()
+                    }
+                    ToolTip.visible: enabled && hovered
+                    ToolTip.delay: 2000
+                    ToolTip.text: "find previous"
                 }
-                ToolTip.visible: enabled && hovered
-                ToolTip.delay: 2000
-                ToolTip.text: "find previous"
-            }
-            TextField {
-                id: searchField
-                placeholderText: "search"
-                Layout.minimumWidth: 200
-                Layout.fillWidth: true
-                Image {
-                    visible: searchField.text !== ""
-                    source: "resources/edit-clear.svg"
-                    anchors {
-                        right: parent.right
-                        top: parent.top
-                        bottom: parent.bottom
-                        margins: 3
-                        rightMargin: 5
+                TextField {
+                    id: searchField
+                    placeholderText: "search"
+                    Layout.minimumWidth: 200
+                    Layout.fillWidth: true
+                    Image {
+                        visible: searchField.text !== ""
+                        source: "resources/edit-clear.svg"
+                        anchors {
+                            right: parent.right
+                            top: parent.top
+                            bottom: parent.bottom
+                            margins: 3
+                            rightMargin: 5
+                        }
+                        TapHandler {
+                            onTapped: searchField.clear()
+                        }
                     }
-                    TapHandler {
-                        onTapped: searchField.clear()
+                }
+                ToolButton {
+                    action: Action {
+                        icon.source: "resources/go-down-search.svg"
+                        shortcut: StandardKey.FindNext
+                        onTriggered: view.searchForward()
                     }
+                    ToolTip.visible: enabled && hovered
+                    ToolTip.delay: 2000
+                    ToolTip.text: "find next"
                 }
             }
-            ToolButton {
-                action: Action {
-                    icon.source: "resources/go-down-search.svg"
-                    onTriggered: view.searchForward()
+            ListView {
+                id: searchResultsList
+                ColumnLayout.fillWidth: true
+                ColumnLayout.fillHeight: true
+                clip: true
+                model: view.searchModel
+                ScrollBar.vertical: ScrollBar { }
+                delegate: ItemDelegate {
+                    width: parent ? parent.width : 0
+                    text: "page " + (page + 1) + ": " + context
+                    highlighted: ListView.isCurrentItem
+                    onClicked: {
+                        searchResultsList.currentIndex = index
+                        view.goToLocation(page, location, 0)
+                        view.searchModel.currentResult = indexOnPage
+                    }
                 }
-                ToolTip.visible: enabled && hovered
-                ToolTip.delay: 2000
-                ToolTip.text: "find next"
             }
         }
     }
diff --git a/examples/pdf/pdfviewer/resources/go-down-search.svg b/examples/pdf/pdfviewer/resources/go-down-search.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ae17ab93b5f5481b8cd19e5135fac2a8ea98987c
--- /dev/null
+++ b/examples/pdf/pdfviewer/resources/go-down-search.svg
@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <defs id="defs3051">
+    <style type="text/css" id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#232629;
+      }
+      </style>
+  </defs>
+ <path style="fill:currentColor;fill-opacity:1;stroke:none"
+     d="M 4.7070312 8 L 4 8.7070312 L 10.125 14.832031 L 12 16.707031 L 13.875 14.832031 L 20 8.7070312 L 19.292969 8 L 13.167969 14.125 L 12 15.292969 L 10.832031 14.125 L 4.7070312 8 z "
+     class="ColorScheme-Text"
+     />
+</svg>
diff --git a/examples/pdf/pdfviewer/resources/go-up-search.svg b/examples/pdf/pdfviewer/resources/go-up-search.svg
new file mode 100644
index 0000000000000000000000000000000000000000..5cc1558731a56fbd07f192d703e57027ac1ace21
--- /dev/null
+++ b/examples/pdf/pdfviewer/resources/go-up-search.svg
@@ -0,0 +1,8 @@
+<svg height="24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <style type="text/css" id="current-color-scheme">
+        .ColorScheme-Text {
+            color:#232629;
+        }
+    </style>
+    <path d="M4.707 16L4 15.293l8-8 8 8-.707.707L12 8.707" class="ColorScheme-Text" fill="currentColor"/>
+</svg>
diff --git a/examples/pdf/pdfviewer/viewer.qml b/examples/pdf/pdfviewer/viewer.qml
index 095bc39859d48fc55614aab7e93d716fa4d44fb8..e94642c5eadce8807a2e158cb8ed81077125d5a8 100644
--- a/examples/pdf/pdfviewer/viewer.qml
+++ b/examples/pdf/pdfviewer/viewer.qml
@@ -57,7 +57,7 @@ import Qt.labs.platform 1.1 as Platform
 
 ApplicationWindow {
     id: root
-    width: 1280
+    width: 800
     height: 1024
     color: "lightgrey"
     title: document.title
@@ -140,14 +140,15 @@ ApplicationWindow {
                 from: 1
                 to: document.pageCount
                 editable: true
-                onValueChanged: pageView.goToPage(value - 1)
+                value: pageView.currentPage + 1
+                onValueModified: pageView.goToPage(value - 1)
                 Shortcut {
                     sequence: StandardKey.MoveToPreviousPage
-                    onActivated: currentPageSB.value--
+                    onActivated: pageView.goToPage(currentPageSB.value - 2)
                 }
                 Shortcut {
                     sequence: StandardKey.MoveToNextPage
-                    onActivated: currentPageSB.value++
+                    onActivated: pageView.goToPage(currentPageSB.value)
                 }
             }
             ToolButton {
@@ -168,30 +169,6 @@ ApplicationWindow {
                     onTriggered: pageView.copySelectionToClipboard()
                 }
             }
-            TextField {
-                id: searchField
-                placeholderText: "search"
-                Layout.minimumWidth: 200
-                Layout.fillWidth: true
-                Image {
-                    visible: searchField.text !== ""
-                    source: "resources/edit-clear.svg"
-                    anchors {
-                        right: parent.right
-                        top: parent.top
-                        bottom: parent.bottom
-                        margins: 3
-                        rightMargin: 5
-                    }
-                    TapHandler {
-                        onTapped: searchField.clear()
-                    }
-                }
-            }
-            Shortcut {
-                sequence: StandardKey.Find
-                onActivated: searchField.forceActiveFocus()
-            }
             Shortcut {
                 sequence: StandardKey.Quit
                 onActivated: Qt.quit()
@@ -223,9 +200,7 @@ ApplicationWindow {
 
     PdfPageView {
         id: pageView
-        // TODO should work but ends up being NaN in QQuickSpinBoxPrivate::setValue() (?!)
-//        onCurrentPageChanged: currentPageSB.value = pageView.currrentPage + 1
-        onCurrentPageReallyChanged: currentPageSB.value = page + 1
+        x: searchDrawer.position * searchDrawer.width // TODO binding gets broken during centering
         document: PdfDocument {
             id: document
             onStatusChanged: if (status === PdfDocument.Error) errorDialog.open()
@@ -233,6 +208,88 @@ ApplicationWindow {
         searchString: searchField.text
     }
 
+    Drawer {
+        id: searchDrawer
+        edge: Qt.LeftEdge
+        modal: false
+        width: searchLayout.implicitWidth
+        y: root.header.height
+        height: root.contentItem.height
+        dim: false
+        Shortcut {
+            sequence: StandardKey.Find
+            onActivated: {
+                searchDrawer.open()
+                searchField.forceActiveFocus()
+            }
+        }
+        ColumnLayout {
+            id: searchLayout
+            anchors.fill: parent
+            anchors.margins: 2
+            RowLayout {
+                ToolButton {
+                    action: Action {
+                        icon.source: "resources/go-up-search.svg"
+                        shortcut: StandardKey.FindPrevious
+                        onTriggered: pageView.searchBack()
+                    }
+                    ToolTip.visible: enabled && hovered
+                    ToolTip.delay: 2000
+                    ToolTip.text: "find previous"
+                }
+                TextField {
+                    id: searchField
+                    placeholderText: "search"
+                    Layout.minimumWidth: 200
+                    Layout.fillWidth: true
+                    Image {
+                        visible: searchField.text !== ""
+                        source: "resources/edit-clear.svg"
+                        anchors {
+                            right: parent.right
+                            top: parent.top
+                            bottom: parent.bottom
+                            margins: 3
+                            rightMargin: 5
+                        }
+                        TapHandler {
+                            onTapped: searchField.clear()
+                        }
+                    }
+                }
+                ToolButton {
+                    action: Action {
+                        icon.source: "resources/go-down-search.svg"
+                        shortcut: StandardKey.FindNext
+                        onTriggered: pageView.searchForward()
+                    }
+                    ToolTip.visible: enabled && hovered
+                    ToolTip.delay: 2000
+                    ToolTip.text: "find next"
+                }
+            }
+            ListView {
+                id: searchResultsList
+                ColumnLayout.fillWidth: true
+                ColumnLayout.fillHeight: true
+                clip: true
+                model: pageView.searchModel
+                ScrollBar.vertical: ScrollBar { }
+                delegate: ItemDelegate {
+                    width: parent ? parent.width : 0
+                    text: "page " + (page + 1) + ": " + context
+                    highlighted: ListView.isCurrentItem
+                    onClicked: {
+                        searchResultsList.currentIndex = index
+                        pageView.goToLocation(page, location, 0)
+                        pageView.searchModel.currentResult = indexOnPage
+                    }
+                }
+            }
+        }
+    }
+
     footer: Label {
         property size implicitPointSize: document.pagePointSize(pageView.currentPage)
         text: "page " + (pageView.currentPage + 1) + " of " + pageView.pageCount +
diff --git a/examples/pdf/pdfviewer/viewer.qrc b/examples/pdf/pdfviewer/viewer.qrc
index fa3561caf0a857175c02157a2262294c43c32b61..9698a2689322a587a90bf65f5909d5ed708e68f3 100644
--- a/examples/pdf/pdfviewer/viewer.qrc
+++ b/examples/pdf/pdfviewer/viewer.qrc
@@ -3,8 +3,10 @@
         <file>viewer.qml</file>
         <file>resources/edit-clear.svg</file>
         <file>resources/edit-copy.svg</file>
+        <file>resources/go-down-search.svg</file>
         <file>resources/go-next-view-page.svg</file>
         <file>resources/go-previous-view-page.svg</file>
+        <file>resources/go-up-search.svg</file>
         <file>resources/rotate-left.svg</file>
         <file>resources/rotate-right.svg</file>
         <file>resources/zoom-in.svg</file>
diff --git a/src/pdf/api/qpdfdestination.h b/src/pdf/api/qpdfdestination.h
index dc5d6314ac951eb793fdacca28dfa556c359cb08..cad0419822dd7c0f3e4f6695a78864606fdadb96 100644
--- a/src/pdf/api/qpdfdestination.h
+++ b/src/pdf/api/qpdfdestination.h
@@ -65,14 +65,14 @@ public:
     QPointF location() const;
     qreal zoom() const;
 
-private:
+protected:
     QPdfDestination();
     QPdfDestination(int page, QPointF location, qreal zoom);
     QPdfDestination(QPdfDestinationPrivate *d);
     friend class QPdfDocument;
     friend class QQuickPdfNavigationStack;
 
-private:
+protected:
     QExplicitlySharedDataPointer<QPdfDestinationPrivate> d;
 };
 
diff --git a/src/pdf/api/qpdfdestination_p.h b/src/pdf/api/qpdfdestination_p.h
index a5aeb804fee51620c06d71af104632bc9b519923..3520fb7951e0c934f459fb3ee58a363ab22d41a7 100644
--- a/src/pdf/api/qpdfdestination_p.h
+++ b/src/pdf/api/qpdfdestination_p.h
@@ -37,6 +37,17 @@
 #ifndef QPDFDESTINATION_P_H
 #define QPDFDESTINATION_P_H
 
+//
+//  W A R N I N G
+//  -------------
+//
+// This file is not part of the Qt API.  It exists purely as an
+// implementation detail.  This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
 #include <QPointF>
 
 QT_BEGIN_NAMESPACE
diff --git a/src/pdf/api/qpdfdocument.h b/src/pdf/api/qpdfdocument.h
index 9d3a4bb198ba8985f4f5fccc6f85d6b454625977..40df181f436fbb2756c101e44adbce9e298b8bb5 100644
--- a/src/pdf/api/qpdfdocument.h
+++ b/src/pdf/api/qpdfdocument.h
@@ -124,6 +124,7 @@ private:
     friend class QPdfBookmarkModelPrivate;
     friend class QPdfLinkModelPrivate;
     friend class QPdfSearchModel;
+    friend class QPdfSearchModelPrivate;
 
     Q_PRIVATE_SLOT(d, void _q_tryLoadingWithSizeFromContentHeader())
     Q_PRIVATE_SLOT(d, void _q_copyFromSequentialSourceDevice())
diff --git a/src/pdf/api/qpdfsearchmodel.h b/src/pdf/api/qpdfsearchmodel.h
index 02d2a20d59691dc94118bd0c6a7ab83028e36254..c8190f192f8e52d64d411eaff369eef60af3e2be 100644
--- a/src/pdf/api/qpdfsearchmodel.h
+++ b/src/pdf/api/qpdfsearchmodel.h
@@ -39,34 +39,56 @@
 
 #include "qtpdfglobal.h"
 #include "qpdfdocument.h"
+#include "qpdfsearchresult.h"
 
-#include <QObject>
+#include <QtCore/qabstractitemmodel.h>
 
 QT_BEGIN_NAMESPACE
 
 class QPdfSearchModelPrivate;
 
-class Q_PDF_EXPORT QPdfSearchModel : public QObject // TODO QAIM?
+class Q_PDF_EXPORT QPdfSearchModel : public QAbstractListModel
 {
     Q_OBJECT
     Q_PROPERTY(QPdfDocument *document READ document WRITE setDocument NOTIFY documentChanged)
+    Q_PROPERTY(QString searchString READ searchString WRITE setSearchString NOTIFY searchStringChanged)
 
 public:
+    enum class Role : int {
+        Page = Qt::UserRole,
+        IndexOnPage,
+        Location,
+        Context,
+        _Count
+    };
+    Q_ENUM(Role)
     explicit QPdfSearchModel(QObject *parent = nullptr);
     ~QPdfSearchModel();
 
-    QVector<QRectF> matches(int page, const QString &searchString);
+    QVector<QPdfSearchResult> resultsOnPage(int page) const;
+    QPdfSearchResult resultAtIndex(int index) const;
 
     QPdfDocument *document() const;
+    QString searchString() const;
+
+    QHash<int, QByteArray> roleNames() const override;
+    int rowCount(const QModelIndex &parent) const override;
+    QVariant data(const QModelIndex &index, int role) const override;
 
 public Q_SLOTS:
+    void setSearchString(QString searchString);
     void setDocument(QPdfDocument *document);
 
 Q_SIGNALS:
     void documentChanged();
+    void searchStringChanged();
+
+protected:
+    void updatePage(int page);
 
 private:
-    QScopedPointer<QPdfSearchModelPrivate> d;
+    QHash<int, QByteArray> m_roleNames;
+    Q_DECLARE_PRIVATE(QPdfSearchModel)
 };
 
 QT_END_NAMESPACE
diff --git a/src/pdf/qpdfsearchmodel_p.h b/src/pdf/api/qpdfsearchmodel_p.h
similarity index 72%
rename from src/pdf/qpdfsearchmodel_p.h
rename to src/pdf/api/qpdfsearchmodel_p.h
index 90490d8e52bcd5c66f83ae656ce6259cd6cb22d6..0855bc2167b4319ff72908047034f236f6f505ad 100644
--- a/src/pdf/qpdfsearchmodel_p.h
+++ b/src/pdf/api/qpdfsearchmodel_p.h
@@ -37,18 +37,46 @@
 #ifndef QPDFSEARCHMODEL_P_H
 #define QPDFSEARCHMODEL_P_H
 
+//
+//  W A R N I N G
+//  -------------
+//
+// This file is not part of the Qt API.  It exists purely as an
+// implementation detail.  This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
 #include "qpdfsearchmodel.h"
+#include "qpdfsearchresult_p.h"
+#include <private/qabstractitemmodel_p.h>
 
 #include "third_party/pdfium/public/fpdfview.h"
 
 QT_BEGIN_NAMESPACE
 
-class QPdfSearchModelPrivate
+class QPdfSearchModelPrivate : public QAbstractItemModelPrivate
 {
+    Q_DECLARE_PUBLIC(QPdfSearchModel)
+
 public:
     QPdfSearchModelPrivate();
+    void clearResults();
+    bool doSearch(int page);
+
+    struct PageAndIndex {
+        int page;
+        int index;
+    };
+    PageAndIndex pageAndIndexForResult(int resultIndex);
+    int rowsBeforePage(int page);
 
     QPdfDocument *document = nullptr;
+    QString searchString;
+    QVector<bool> pagesSearched;
+    QVector<QVector<QPdfSearchResult>> searchResults;
+    int rowCountSoFar = 0;
 };
 
 QT_END_NAMESPACE
diff --git a/src/pdf/api/qpdfsearchresult.h b/src/pdf/api/qpdfsearchresult.h
new file mode 100644
index 0000000000000000000000000000000000000000..db7af3dd9eba376edaab4db67e5dea5b5ccd044c
--- /dev/null
+++ b/src/pdf/api/qpdfsearchresult.h
@@ -0,0 +1,75 @@
+/****************************************************************************
+**
+** Copyright (C) 2020 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the QtPDF module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QPDFSEARCHRESULT_H
+#define QPDFSEARCHRESULT_H
+
+#include "qpdfdestination.h"
+
+#include <QtCore/qdebug.h>
+#include <QtCore/qrect.h>
+#include <QtCore/qvector.h>
+
+QT_BEGIN_NAMESPACE
+
+class QPdfSearchResultPrivate;
+
+class Q_PDF_EXPORT QPdfSearchResult : public QPdfDestination
+{
+    Q_GADGET
+    Q_PROPERTY(QString context READ context)
+    Q_PROPERTY(QVector<QRectF> rectangles READ rectangles)
+
+public:
+    QPdfSearchResult();
+    ~QPdfSearchResult() {}
+
+    QString context() const;
+    QVector<QRectF> rectangles() const;
+
+private:
+    QPdfSearchResult(int page, QVector<QRectF> rects, QString context);
+    QPdfSearchResult(QPdfSearchResultPrivate *d);
+    friend class QPdfDocument;
+    friend class QPdfSearchModelPrivate;
+    friend class QQuickPdfNavigationStack;
+};
+
+Q_PDF_EXPORT QDebug operator<<(QDebug, const QPdfSearchResult &);
+
+QT_END_NAMESPACE
+
+#endif // QPDFSEARCHRESULT_H
diff --git a/src/pdf/api/qpdfsearchresult_p.h b/src/pdf/api/qpdfsearchresult_p.h
new file mode 100644
index 0000000000000000000000000000000000000000..a0f8e44579de0703ef6487ed033345ebfcef545a
--- /dev/null
+++ b/src/pdf/api/qpdfsearchresult_p.h
@@ -0,0 +1,70 @@
+/****************************************************************************
+**
+** Copyright (C) 2020 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the QtPDF module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QPDFSEARCHRESULT_P_H
+#define QPDFSEARCHRESULT_P_H
+
+//
+//  W A R N I N G
+//  -------------
+//
+// This file is not part of the Qt API.  It exists purely as an
+// implementation detail.  This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include "qpdfdestination_p.h"
+
+QT_BEGIN_NAMESPACE
+
+class QPdfSearchResultPrivate : public QPdfDestinationPrivate
+{
+public:
+    QPdfSearchResultPrivate() = default;
+    QPdfSearchResultPrivate(int page, QVector<QRectF> rects, QString context) :
+        QPdfDestinationPrivate(page, rects.first().topLeft(), 0),
+        context(context),
+        rects(rects) {}
+
+    QString context;
+    QVector<QRectF> rects;
+};
+
+QT_END_NAMESPACE
+
+#endif // QPDFSEARCHRESULT_P_H
diff --git a/src/pdf/pdfcore.pro b/src/pdf/pdfcore.pro
index 951b5699f01c9c5715f7e6bb65cff0b458ecd522..e723a02fd2f86388701d4ef0f2c2eb3287447daa 100644
--- a/src/pdf/pdfcore.pro
+++ b/src/pdf/pdfcore.pro
@@ -66,6 +66,7 @@ SOURCES += \
     qpdfpagenavigation.cpp \
     qpdfpagerenderer.cpp \
     qpdfsearchmodel.cpp \
+    qpdfsearchresult.cpp \
     qpdfselection.cpp \
 
 # all "public" headers must be in "api" for sync script and to hide auto generated headers
@@ -85,7 +86,9 @@ HEADERS += \
     api/qpdfpagenavigation.h \
     api/qpdfpagerenderer.h \
     api/qpdfsearchmodel.h \
-    qpdfsearchmodel_p.h \
+    api/qpdfsearchmodel_p.h \
+    api/qpdfsearchresult.h \
+    api/qpdfsearchresult_p.h \
     api/qpdfselection.h \
     api/qpdfselection_p.h \
 
diff --git a/src/pdf/qpdfsearchmodel.cpp b/src/pdf/qpdfsearchmodel.cpp
index 9010d76d320d2acafbda2903f67a3964eb8be821..aa19af5b128b178473299e55c03d85907be52a09 100644
--- a/src/pdf/qpdfsearchmodel.cpp
+++ b/src/pdf/qpdfsearchmodel.cpp
@@ -34,80 +34,257 @@
 **
 ****************************************************************************/
 
+#include "qpdfdestination.h"
+#include "qpdfdocument_p.h"
 #include "qpdfsearchmodel.h"
 #include "qpdfsearchmodel_p.h"
-#include "qpdfdocument_p.h"
+#include "qpdfsearchresult_p.h"
 
 #include "third_party/pdfium/public/fpdf_doc.h"
 #include "third_party/pdfium/public/fpdf_text.h"
 
-#include <QLoggingCategory>
+#include <QtCore/qelapsedtimer.h>
+#include <QtCore/qloggingcategory.h>
+#include <QtCore/QMetaEnum>
 
 QT_BEGIN_NAMESPACE
 
 Q_LOGGING_CATEGORY(qLcS, "qt.pdf.search")
 
+static const int ContextChars = 20;
+static const double CharacterHitTolerance = 6.0;
+
 QPdfSearchModel::QPdfSearchModel(QObject *parent)
-    : QObject(parent),
-      d(new QPdfSearchModelPrivate())
+    : QAbstractListModel(*(new QPdfSearchModelPrivate()), parent)
 {
+    QMetaEnum rolesMetaEnum = metaObject()->enumerator(metaObject()->indexOfEnumerator("Role"));
+    for (int r = Qt::UserRole; r < int(Role::_Count); ++r) {
+        QByteArray roleName = QByteArray(rolesMetaEnum.valueToKey(r));
+        if (roleName.isEmpty())
+            continue;
+        roleName[0] = QChar::toLower(roleName[0]);
+        m_roleNames.insert(r, roleName);
+    }
 }
 
 QPdfSearchModel::~QPdfSearchModel() {}
 
-QVector<QRectF> QPdfSearchModel::matches(int page, const QString &searchString)
+QHash<int, QByteArray> QPdfSearchModel::roleNames() const
+{
+    return m_roleNames;
+}
+
+int QPdfSearchModel::rowCount(const QModelIndex &parent) const
+{
+    Q_D(const QPdfSearchModel);
+    Q_UNUSED(parent)
+    return d->rowCountSoFar;
+}
+
+QVariant QPdfSearchModel::data(const QModelIndex &index, int role) const
 {
+    Q_D(const QPdfSearchModel);
+    const auto pi = const_cast<QPdfSearchModelPrivate*>(d)->pageAndIndexForResult(index.row());
+    if (pi.page < 0)
+        return QVariant();
+    switch (Role(role)) {
+    case Role::Page:
+        return pi.page;
+    case Role::IndexOnPage:
+        return pi.index;
+    case Role::Location:
+        return d->searchResults[pi.page][pi.index].location();
+    case Role::Context:
+        return d->searchResults[pi.page][pi.index].context();
+    case Role::_Count:
+        break;
+    }
+    if (role == Qt::DisplayRole)
+        return d->searchResults[pi.page][pi.index].context();
+    return QVariant();
+}
+
+void QPdfSearchModel::updatePage(int page)
+{
+    Q_D(QPdfSearchModel);
+    d->doSearch(page);
+}
+
+QString QPdfSearchModel::searchString() const
+{
+    Q_D(const QPdfSearchModel);
+    return d->searchString;
+}
+
+void QPdfSearchModel::setSearchString(QString searchString)
+{
+    Q_D(QPdfSearchModel);
+    if (d->searchString == searchString)
+        return;
+
+    d->searchString = searchString;
+    emit searchStringChanged();
+    beginResetModel();
+    d->clearResults();
+    endResetModel();
+}
+
+QVector<QPdfSearchResult> QPdfSearchModel::resultsOnPage(int page) const
+{
+    Q_D(const QPdfSearchModel);
+    const_cast<QPdfSearchModelPrivate *>(d)->doSearch(page);
+    if (d->searchResults.count() <= page)
+        return {};
+    return d->searchResults[page];
+}
+
+QPdfSearchResult QPdfSearchModel::resultAtIndex(int index) const
+{
+    Q_D(const QPdfSearchModel);
+    const auto pi = const_cast<QPdfSearchModelPrivate*>(d)->pageAndIndexForResult(index);
+    if (pi.page < 0)
+        return QPdfSearchResult();
+    return d->searchResults[pi.page][pi.index];
+}
+
+QPdfDocument *QPdfSearchModel::document() const
+{
+    Q_D(const QPdfSearchModel);
+    return d->document;
+}
+
+void QPdfSearchModel::setDocument(QPdfDocument *document)
+{
+    Q_D(QPdfSearchModel);
+    if (d->document == document)
+        return;
+
+    d->document = document;
+    emit documentChanged();
+    d->clearResults();
+}
+
+QPdfSearchModelPrivate::QPdfSearchModelPrivate()
+{
+}
+
+void QPdfSearchModelPrivate::clearResults()
+{
+    rowCountSoFar = 0;
+    searchResults.clear();
+    pagesSearched.clear();
+    if (document) {
+        searchResults.resize(document->pageCount());
+        pagesSearched.resize(document->pageCount());
+    } else {
+        searchResults.resize(0);
+        pagesSearched.resize(0);
+    }
+}
+
+bool QPdfSearchModelPrivate::doSearch(int page)
+{
+    if (page < 0 || page >= pagesSearched.count() || searchString.isEmpty())
+        return false;
+    if (pagesSearched[page])
+        return true;
+    Q_Q(QPdfSearchModel);
+
     const QPdfMutexLocker lock;
-    FPDF_PAGE pdfPage = FPDF_LoadPage(d->document->d->doc, page);
+    QElapsedTimer timer;
+    timer.start();
+    FPDF_PAGE pdfPage = FPDF_LoadPage(document->d->doc, page);
     if (!pdfPage) {
         qWarning() << "failed to load page" << page;
-        return {};
+        return false;
     }
     double pageHeight = FPDF_GetPageHeight(pdfPage);
     FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
     if (!textPage) {
         qWarning() << "failed to load text of page" << page;
         FPDF_ClosePage(pdfPage);
-        return {};
+        return false;
     }
-    QVector<QRectF> ret;
-    if (searchString.isEmpty())
-        return ret;
     FPDF_SCHHANDLE sh = FPDFText_FindStart(textPage, searchString.utf16(), 0, 0);
+    QVector<QPdfSearchResult> newSearchResults;
     while (FPDFText_FindNext(sh)) {
         int idx = FPDFText_GetSchResultIndex(sh);
         int count = FPDFText_GetSchCount(sh);
         int rectCount = FPDFText_CountRects(textPage, idx, count);
-        qCDebug(qLcS) << searchString << ": matched" << count << "chars @" << idx << "across" << rectCount << "rects";
+        QVector<QRectF> rects;
+        int startIndex = -1;
+        int endIndex = -1;
         for (int r = 0; r < rectCount; ++r) {
             double left, top, right, bottom;
             FPDFText_GetRect(textPage, r, &left, &top, &right, &bottom);
-            ret << QRectF(left, pageHeight - top, right - left, top - bottom);
-            qCDebug(qLcS) << ret.last();
+            rects << QRectF(left, pageHeight - top, right - left, top - bottom);
+            if (r == 0) {
+                startIndex = FPDFText_GetCharIndexAtPos(textPage, left, top,
+                        CharacterHitTolerance, CharacterHitTolerance);
+            }
+            if (r == rectCount - 1) {
+                endIndex = FPDFText_GetCharIndexAtPos(textPage, right, top,
+                        CharacterHitTolerance, CharacterHitTolerance);
+            }
+            qCDebug(qLcS) << rects.last() << "char idx" << startIndex << "->" << endIndex;
+        }
+        QString context;
+        if (startIndex >= 0 || endIndex >= 0) {
+            startIndex = qMax(0, startIndex - ContextChars);
+            endIndex += ContextChars;
+            int count = endIndex - startIndex + 1;
+            if (count > 0) {
+                QVector<ushort> buf(count + 1);
+                int len = FPDFText_GetText(textPage, startIndex, count, buf.data());
+                Q_ASSERT(len - 1 <= count); // len is number of characters written, including the terminator
+                context = QString::fromUtf16(buf.constData(), len - 1);
+                context = context.replace(QLatin1Char('\n'), QLatin1Char(' '));
+                context = context.replace(searchString,
+                                          QLatin1String("<b>") + searchString + QLatin1String("</b>"));
+            }
         }
+        newSearchResults << QPdfSearchResult(page, rects, context);
     }
     FPDFText_FindClose(sh);
     FPDFText_ClosePage(textPage);
     FPDF_ClosePage(pdfPage);
+    qCDebug(qLcS) << searchString << "took" << timer.elapsed() << "ms to find"
+                  << newSearchResults.count() << "results on page" << page;
 
-    return ret;
-}
-
-QPdfDocument *QPdfSearchModel::document() const
-{
-    return d->document;
+    pagesSearched[page] = true;
+    searchResults[page] = newSearchResults;
+    if (newSearchResults.count() > 0) {
+        int rowsBefore = rowsBeforePage(page);
+        qCDebug(qLcS) << "from row" << rowsBefore << "rowCount" << rowCountSoFar << "increasing by" << newSearchResults.count();
+        rowCountSoFar += newSearchResults.count();
+        q->beginInsertRows(QModelIndex(), rowsBefore, rowsBefore + newSearchResults.count() - 1);
+        q->endInsertRows();
+    }
+    return true;
 }
 
-void QPdfSearchModel::setDocument(QPdfDocument *document)
+QPdfSearchModelPrivate::PageAndIndex QPdfSearchModelPrivate::pageAndIndexForResult(int resultIndex)
 {
-    if (d->document == document)
-        return;
-    d->document = document;
-    emit documentChanged();
+    const int pageCount = document->pageCount();
+    int totalSoFar = 0;
+    int previousTotalSoFar = 0;
+    for (int page = 0; page < pageCount; ++page) {
+        if (!pagesSearched[page])
+            doSearch(page);
+        totalSoFar += searchResults[page].count();
+        if (totalSoFar > resultIndex)
+            return {page, resultIndex - previousTotalSoFar};
+        previousTotalSoFar = totalSoFar;
+    }
+    return {-1, -1};
 }
 
-QPdfSearchModelPrivate::QPdfSearchModelPrivate()
+int QPdfSearchModelPrivate::rowsBeforePage(int page)
 {
+    int ret = 0;
+    for (int i = 0; i < page; ++i)
+        ret += searchResults[i].count();
+    return ret;
 }
 
 QT_END_NAMESPACE
diff --git a/src/pdf/qpdfsearchresult.cpp b/src/pdf/qpdfsearchresult.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..1164a1d430cc2a96fda55151419821ab255bd803
--- /dev/null
+++ b/src/pdf/qpdfsearchresult.cpp
@@ -0,0 +1,74 @@
+/****************************************************************************
+**
+** Copyright (C) 2020 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the QtPDF module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL3$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPLv3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or later as published by the Free
+** Software Foundation and appearing in the file LICENSE.GPL included in
+** the packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 2.0 requirements will be
+** met: http://www.gnu.org/licenses/gpl-2.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "qpdfsearchresult.h"
+#include "qpdfsearchresult_p.h"
+
+QT_BEGIN_NAMESPACE
+
+QPdfSearchResult::QPdfSearchResult() :
+    QPdfSearchResult(new QPdfSearchResultPrivate()) { }
+
+QPdfSearchResult::QPdfSearchResult(int page, QVector<QRectF> rects, QString context) :
+    QPdfSearchResult(new QPdfSearchResultPrivate(page, rects, context)) { }
+
+QPdfSearchResult::QPdfSearchResult(QPdfSearchResultPrivate *d) :
+    QPdfDestination(static_cast<QPdfDestinationPrivate *>(d)) { }
+
+QString QPdfSearchResult::context() const
+{
+    return static_cast<QPdfSearchResultPrivate *>(d.data())->context;
+}
+
+QVector<QRectF> QPdfSearchResult::rectangles() const
+{
+    return static_cast<QPdfSearchResultPrivate *>(d.data())->rects;
+}
+
+QDebug operator<<(QDebug dbg, const QPdfSearchResult &searchResult)
+{
+    QDebugStateSaver saver(dbg);
+    dbg.nospace();
+    dbg << "QPdfSearchResult(page=" << searchResult.page()
+        << " context=" << searchResult.context()
+        << " rects=" << searchResult.rectangles();
+    dbg << ')';
+    return dbg;
+}
+
+QT_END_NAMESPACE
+
+#include "moc_qpdfsearchresult.cpp"
diff --git a/src/pdf/quick/qml/PdfMultiPageView.qml b/src/pdf/quick/qml/PdfMultiPageView.qml
index bc5134267bcdb539ac7a6022f5d3a6de902ff51c..b64f4457671b7ac04264c1c81825a22c99912d50 100644
--- a/src/pdf/quick/qml/PdfMultiPageView.qml
+++ b/src/pdf/quick/qml/PdfMultiPageView.qml
@@ -104,39 +104,10 @@ Item {
     }
 
     // text search
+    property alias searchModel: searchModel
     property alias searchString: searchModel.searchString
-    property bool searchBackEnabled: searchModel.currentResult > 0
-    property bool searchForwardEnabled: searchModel.currentResult < searchModel.matchGeometry.length - 1
-    function searchBack() {
-        if (searchModel.currentResult > 0) {
-            --searchModel.currentResult
-        } else {
-            searchModel.deferRendering = true // save time while we are searching
-            while (searchModel.currentResult <= 0) {
-                if (navigationStack.currentPage > 0)
-                    goToPage(navigationStack.currentPage - 1)
-                else
-                    goToPage(document.pageCount - 1)
-                searchModel.currentResult = searchModel.matchGeometry.length - 1
-            }
-            searchModel.deferRendering = false
-        }
-    }
-    function searchForward() {
-        if (searchModel.currentResult < searchModel.matchGeometry.length - 1) {
-            ++searchModel.currentResult
-        } else {
-            searchModel.deferRendering = true // save time while we are searching
-            while (searchModel.currentResult >= searchModel.matchGeometry.length - 1) {
-                searchModel.currentResult = 0
-                if (navigationStack.currentPage < document.pageCount - 1)
-                    goToPage(navigationStack.currentPage + 1)
-                else
-                    goToPage(0)
-            }
-            searchModel.deferRendering = false
-        }
-    }
+    function searchBack() { --searchModel.currentResult }
+    function searchForward() { ++searchModel.currentResult }
 
     id: root
     ListView {
@@ -159,7 +130,7 @@ Item {
             property real pageScale: image.paintedWidth / pagePointSize.width
             Image {
                 id: image
-                source: searchModel.deferRendering ? "" : document.source
+                source: document.source
                 currentFrame: index
                 asynchronous: true
                 fillMode: Image.PreserveAspectFit
@@ -178,31 +149,36 @@ Item {
             Shape {
                 anchors.fill: parent
                 opacity: 0.25
-                visible: image.status === Image.Ready && searchModel.page == index
+                visible: image.status === Image.Ready
                 ShapePath {
                     strokeWidth: 1
-                    strokeColor: "steelblue"
-                    fillColor: "lightsteelblue"
+                    strokeColor: "cyan"
+                    fillColor: "steelblue"
                     scale: Qt.size(paper.pageScale, paper.pageScale)
                     PathMultiline {
-                        paths: searchModel.matchGeometry
+                        paths: searchModel.boundingPolygonsOnPage(index)
                     }
                 }
                 ShapePath {
-                    strokeWidth: 1
-                    strokeColor: "blue"
-                    fillColor: "cyan"
+                    fillColor: "orange"
                     scale: Qt.size(paper.pageScale, paper.pageScale)
-                    PathPolyline {
-                        path: searchModel.matchGeometry[searchModel.currentResult]
+                    PathMultiline {
+                        id: selectionBoundaries
+                        paths: selection.geometry
                     }
                 }
+            }
+            Shape {
+                anchors.fill: parent
+                opacity: 0.5
+                visible: image.status === Image.Ready && searchModel.currentPage === index
                 ShapePath {
-                    fillColor: "orange"
+                    strokeWidth: 1
+                    strokeColor: "blue"
+                    fillColor: "cyan"
                     scale: Qt.size(paper.pageScale, paper.pageScale)
                     PathMultiline {
-                        id: selectionBoundaries
-                        paths: selection.geometry
+                        paths: searchModel.currentResultBoundingPolygons
                     }
                 }
             }
@@ -297,7 +273,10 @@ Item {
     PdfNavigationStack {
         id: navigationStack
         onJumped: listView.currentIndex = page
-        onCurrentPageChanged: listView.positionViewAtIndex(currentPage, ListView.Beginning)
+        onCurrentPageChanged: {
+            listView.positionViewAtIndex(currentPage, ListView.Beginning)
+            searchModel.currentPage = currentPage
+        }
         onCurrentLocationChanged: listView.contentY += currentLocation.y // currentPageChanged() MUST occur first!
         onCurrentZoomChanged: root.renderScale = currentZoom
         // TODO deal with horizontal location (need another Flickable probably)
@@ -305,9 +284,6 @@ Item {
     PdfSearchModel {
         id: searchModel
         document: root.document === undefined ? null : root.document
-        page: navigationStack.currentPage
-        searchString: root.searchString
-        property int currentResult: 0
-        property bool deferRendering: false
+        onCurrentPageChanged: root.goToPage(currentPage)
     }
 }
diff --git a/src/pdf/quick/qml/PdfPageView.qml b/src/pdf/quick/qml/PdfPageView.qml
index d03e9dc9dca14e918c6f688799fb84214435bd24..f4d7da0af9b9ad517eb86611ad4edfd8230f37e3 100644
--- a/src/pdf/quick/qml/PdfPageView.qml
+++ b/src/pdf/quick/qml/PdfPageView.qml
@@ -50,15 +50,18 @@ Rectangle {
     property alias sourceSize: image.sourceSize
     property alias currentPage: navigationStack.currentPage
     property alias pageCount: image.frameCount
-    property alias searchString: searchModel.searchString
     property alias selectedText: selection.text
     property alias status: image.status
     property alias backEnabled: navigationStack.backAvailable
     property alias forwardEnabled: navigationStack.forwardAvailable
     function back() { navigationStack.back() }
     function forward() { navigationStack.forward() }
-    function goToPage(page) { navigationStack.push(page, Qt.point(0, 0), renderScale) }
-    signal currentPageReallyChanged(page: int)
+    function goToPage(page) { goToLocation(page, Qt.point(0, 0), 0) }
+    function goToLocation(page, location, zoom) {
+        if (zoom > 0)
+            paper.renderScale = zoom
+        navigationStack.push(page, location, zoom)
+    }
 
     property real __pageScale: image.paintedWidth / document.pagePointSize(navigationStack.currentPage).width
 
@@ -107,6 +110,12 @@ Rectangle {
         paper.scale = 1
     }
 
+    // text search
+    property alias searchModel: searchModel
+    property alias searchString: searchModel.searchString
+    function searchBack() { --searchModel.currentResult }
+    function searchForward() { ++searchModel.currentResult }
+
     PdfSelection {
         id: selection
         document: paper.document
@@ -121,13 +130,16 @@ Rectangle {
 
     PdfSearchModel {
         id: searchModel
-        document: paper.document
-        page: navigationStack.currentPage
+        document: paper.document === undefined ? null : paper.document
+        currentPage: navigationStack.currentPage
+        onCurrentPageChanged: paper.goToPage(currentPage)
     }
 
     PdfNavigationStack {
         id: navigationStack
-        onCurrentPageChanged: paper.currentPageReallyChanged(navigationStack.currentPage)
+        // TODO onCurrentLocationChanged: position currentLocation.x and .y in middle // currentPageChanged() MUST occur first!
+        onCurrentZoomChanged: paper.renderScale = currentZoom
+        // TODO deal with horizontal location (need WheelHandler or Flickable probably)
     }
 
     Image {
@@ -168,19 +180,26 @@ Rectangle {
         visible: image.status === Image.Ready
         ShapePath {
             strokeWidth: 1
-            strokeColor: "blue"
+            strokeColor: "cyan"
+            fillColor: "steelblue"
+            scale: Qt.size(paper.__pageScale, paper.__pageScale)
+            PathMultiline {
+                paths: searchModel.currentPageBoundingPolygons
+            }
+        }
+        ShapePath {
+            strokeWidth: 1
+            strokeColor: "orange"
             fillColor: "cyan"
             scale: Qt.size(paper.__pageScale, paper.__pageScale)
             PathMultiline {
-                id: searchResultBoundaries
-                paths: searchModel.matchGeometry
+                paths: searchModel.currentResultBoundingPolygons
             }
         }
         ShapePath {
             fillColor: "orange"
             scale: Qt.size(paper.__pageScale, paper.__pageScale)
             PathMultiline {
-                id: selectionBoundaries
                 paths: selection.geometry
             }
         }
diff --git a/src/pdf/quick/qquickpdfsearchmodel.cpp b/src/pdf/quick/qquickpdfsearchmodel.cpp
index 8b0e8867354c8ec4a6d9c5cba8b4fd0a57bc6625..ec998ef0c63ef7cd0cd97ee974ac456b41727ee1 100644
--- a/src/pdf/quick/qquickpdfsearchmodel.cpp
+++ b/src/pdf/quick/qquickpdfsearchmodel.cpp
@@ -35,13 +35,12 @@
 ****************************************************************************/
 
 #include "qquickpdfsearchmodel_p.h"
-#include <QQuickItem>
-#include <QQmlEngine>
-#include <QStandardPaths>
-#include <private/qguiapplication_p.h>
+#include <QtCore/qloggingcategory.h>
 
 QT_BEGIN_NAMESPACE
 
+Q_LOGGING_CATEGORY(qLcS, "qt.pdf.search")
+
 /*!
     \qmltype PdfSearchModel
     \instantiates QQuickPdfSearchModel
@@ -57,6 +56,8 @@ QT_BEGIN_NAMESPACE
 QQuickPdfSearchModel::QQuickPdfSearchModel(QObject *parent)
     : QPdfSearchModel(parent)
 {
+    connect(this, &QPdfSearchModel::searchStringChanged,
+            this, &QQuickPdfSearchModel::onResultsChanged);
 }
 
 QQuickPdfDocument *QQuickPdfSearchModel::document() const
@@ -68,16 +69,19 @@ void QQuickPdfSearchModel::setDocument(QQuickPdfDocument *document)
 {
     if (document == m_quickDocument)
         return;
+
     m_quickDocument = document;
     QPdfSearchModel::setDocument(&document->m_doc);
 }
 
 /*!
-    \qmlproperty list<list<point>> PdfSearchModel::matchGeometry
+    \qmlproperty list<list<point>> PdfSearchModel::currentResultBoundingPolygons
 
     A set of paths in a form that can be bound to the \c paths property of a
     \l {QtQuick::PathMultiline}{PathMultiline} instance to render a batch of
-    rectangles around all the locations where search results are found:
+    rectangles around the regions comprising the search result \l currentResult
+    on \l currentPage.  This is normally used to highlight one search result
+    at a time, in a UI that allows stepping through the results:
 
     \qml
     PdfDocument {
@@ -86,12 +90,13 @@ void QQuickPdfSearchModel::setDocument(QQuickPdfDocument *document)
     PdfSearchModel {
         id: searchModel
         document: doc
-        page: doc.currentPage
+        currentPage: view.currentPage
+        currentResult: ...
     }
     Shape {
         ShapePath {
             PathMultiline {
-                paths: searchModel.matchGeometry
+                paths: searchModel.currentResultBoundingPolygons
             }
         }
     }
@@ -99,67 +104,174 @@ void QQuickPdfSearchModel::setDocument(QQuickPdfDocument *document)
 
     \sa PathMultiline
 */
-QVector<QPolygonF> QQuickPdfSearchModel::matchGeometry() const
+QVector<QPolygonF> QQuickPdfSearchModel::currentResultBoundingPolygons() const
 {
-    return m_matchGeometry;
+    QVector<QPolygonF> ret;
+    const auto &results = const_cast<QQuickPdfSearchModel *>(this)->resultsOnPage(m_currentPage);
+    if (m_currentResult < 0 || m_currentResult >= results.count())
+        return ret;
+    const auto result = results[m_currentResult];
+    for (auto rect : result.rectangles())
+        ret << QPolygonF(rect);
+    return ret;
 }
 
-/*!
-    \qmlproperty string PdfSearchModel::searchString
-
-    The string to search for.
-*/
-QString QQuickPdfSearchModel::searchString() const
+void QQuickPdfSearchModel::onResultsChanged()
 {
-    return m_searchString;
+    emit currentPageBoundingPolygonsChanged();
+    emit currentResultBoundingPolygonsChanged();
 }
 
-void QQuickPdfSearchModel::setSearchString(QString searchString)
-{
-    if (m_searchString == searchString)
-        return;
+/*!
+    \qmlproperty list<list<point>> PdfSearchModel::currentPageBoundingPolygons
+
+    A set of paths in a form that can be bound to the \c paths property of a
+    \l {QtQuick::PathMultiline}{PathMultiline} instance to render a batch of
+    rectangles around all the regions where search results are found on
+    \l currentPage:
+
+    \qml
+    PdfDocument {
+        id: doc
+    }
+    PdfSearchModel {
+        id: searchModel
+        document: doc
+    }
+    Shape {
+        ShapePath {
+            PathMultiline {
+                paths: searchModel.matchGeometry(view.currentPage)
+            }
+        }
+    }
+    \endqml
 
-    m_searchString = searchString;
-    emit searchStringChanged();
-    updateResults();
+    \sa PathMultiline
+*/
+QVector<QPolygonF> QQuickPdfSearchModel::currentPageBoundingPolygons() const
+{
+    return const_cast<QQuickPdfSearchModel *>(this)->boundingPolygonsOnPage(m_currentPage);
 }
 
 /*!
-    \qmlproperty int PdfSearchModel::page
+    \qmlfunction list<list<point>> PdfSearchModel::boundingPolygonsOnPage(int page)
 
-    The page number on which to search.
+    Returns a set of paths in a form that can be bound to the \c paths property of a
+    \l {QtQuick::PathMultiline}{PathMultiline} instance to render a batch of
+    rectangles around all the locations where search results are found:
 
-    \sa QtQuick::Image::currentFrame
+    \qml
+    PdfDocument {
+        id: doc
+    }
+    PdfSearchModel {
+        id: searchModel
+        document: doc
+    }
+    Shape {
+        ShapePath {
+            PathMultiline {
+                paths: searchModel.matchGeometry(view.currentPage)
+            }
+        }
+    }
+    \endqml
+
+    \sa PathMultiline
 */
-int QQuickPdfSearchModel::page() const
+QVector<QPolygonF> QQuickPdfSearchModel::boundingPolygonsOnPage(int page)
 {
-    return m_page;
+    if (!document() || searchString().isEmpty() || page < 0 || page > document()->pageCount())
+        return {};
+
+    updatePage(page);
+
+    QVector<QPolygonF> ret;
+    auto m = QPdfSearchModel::resultsOnPage(page);
+    for (auto result : m) {
+        for (auto rect : result.rectangles())
+            ret << QPolygonF(rect);
+    }
+
+    return ret;
 }
 
-void QQuickPdfSearchModel::setPage(int page)
+/*!
+    \qmlproperty int PdfSearchModel::currentPage
+
+    The page on which \l currentMatchGeometry should provide filtered search results.
+*/
+void QQuickPdfSearchModel::setCurrentPage(int currentPage)
 {
-    if (m_page == page)
+    if (m_currentPage == currentPage)
         return;
 
-    m_page = page;
-    emit pageChanged();
-    updateResults();
+    if (currentPage < 0)
+        currentPage = document()->pageCount() - 1;
+    else if (currentPage >= document()->pageCount())
+        currentPage = 0;
+
+    m_currentPage = currentPage;
+    if (!m_suspendSignals) {
+        emit currentPageChanged();
+        onResultsChanged();
+    }
 }
 
-void QQuickPdfSearchModel::updateResults()
+/*!
+    \qmlproperty int PdfSearchModel::currentResult
+
+    The result index on \l currentPage for which \l currentResultBoundingPolygons
+    should provide the regions to highlight.
+*/
+void QQuickPdfSearchModel::setCurrentResult(int currentResult)
 {
-    if (!document() || (m_searchString.isEmpty() && !m_matchGeometry.isEmpty()) || m_page < 0 || m_page > document()->pageCount()) {
-        m_matchGeometry.clear();
-        emit matchGeometryChanged();
-    }
-    QVector<QRectF> m = QPdfSearchModel::matches(m_page, m_searchString);
-    QVector<QPolygonF> matches;
-    for (QRectF r : m)
-        matches << QPolygonF(r);
-    if (matches != m_matchGeometry) {
-        m_matchGeometry = matches;
-        emit matchGeometryChanged();
+    if (m_currentResult == currentResult)
+        return;
+
+    int currentResultWas = currentResult;
+    int currentPageWas = m_currentPage;
+    if (currentResult < 0) {
+        setCurrentPage(m_currentPage - 1);
+        while (resultsOnPage(m_currentPage).count() == 0 && m_currentPage != currentPageWas) {
+            m_suspendSignals = true;
+            setCurrentPage(m_currentPage - 1);
+        }
+        if (m_suspendSignals) {
+            emit currentPageChanged();
+            m_suspendSignals = false;
+        }
+        const auto results = resultsOnPage(m_currentPage);
+        currentResult = results.count() - 1;
+    } else {
+        const auto results = resultsOnPage(m_currentPage);
+        if (currentResult >= results.count()) {
+            setCurrentPage(m_currentPage + 1);
+            while (resultsOnPage(m_currentPage).count() == 0 && m_currentPage != currentPageWas) {
+                m_suspendSignals = true;
+                setCurrentPage(m_currentPage + 1);
+            }
+            if (m_suspendSignals) {
+                emit currentPageChanged();
+                m_suspendSignals = false;
+            }
+            currentResult = 0;
+        }
     }
+    qCDebug(qLcS) << "currentResult was" << m_currentResult
+                  << "requested" << currentResultWas << "on page" << currentPageWas
+                  << "->" << currentResult << "on page" << m_currentPage;
+
+    m_currentResult = currentResult;
+    emit currentResultChanged();
+    emit currentResultBoundingPolygonsChanged();
 }
 
+/*!
+    \qmlproperty string PdfSearchModel::searchString
+
+    The string to search for.
+*/
+
 QT_END_NAMESPACE
diff --git a/src/pdf/quick/qquickpdfsearchmodel_p.h b/src/pdf/quick/qquickpdfsearchmodel_p.h
index 82a6289d028f21938d8237e711701a0f3973e04a..3e05f80e36ba19012097cd464fa7a0c3f6233baa 100644
--- a/src/pdf/quick/qquickpdfsearchmodel_p.h
+++ b/src/pdf/quick/qquickpdfsearchmodel_p.h
@@ -51,7 +51,7 @@
 #include "qquickpdfdocument_p.h"
 #include "../api/qpdfsearchmodel.h"
 
-#include <QVariant>
+#include <QtCore/qvariant.h>
 #include <QtQml/qqml.h>
 
 QT_BEGIN_NAMESPACE
@@ -60,9 +60,10 @@ class QQuickPdfSearchModel : public QPdfSearchModel
 {
     Q_OBJECT
     Q_PROPERTY(QQuickPdfDocument *document READ document WRITE setDocument NOTIFY documentChanged)
-    Q_PROPERTY(int page READ page WRITE setPage NOTIFY pageChanged)
-    Q_PROPERTY(QString searchString READ searchString WRITE setSearchString NOTIFY searchStringChanged)
-    Q_PROPERTY(QVector<QPolygonF> matchGeometry READ matchGeometry NOTIFY matchGeometryChanged)
+    Q_PROPERTY(int currentPage READ currentPage WRITE setCurrentPage NOTIFY currentPageChanged)
+    Q_PROPERTY(int currentResult READ currentResult WRITE setCurrentResult NOTIFY currentResultChanged)
+    Q_PROPERTY(QVector<QPolygonF> currentPageBoundingPolygons READ currentPageBoundingPolygons NOTIFY currentPageBoundingPolygonsChanged)
+    Q_PROPERTY(QVector<QPolygonF> currentResultBoundingPolygons READ currentResultBoundingPolygons NOTIFY currentResultBoundingPolygonsChanged)
 
 public:
     explicit QQuickPdfSearchModel(QObject *parent = nullptr);
@@ -70,28 +71,33 @@ public:
     QQuickPdfDocument *document() const;
     void setDocument(QQuickPdfDocument * document);
 
-    int page() const;
-    void setPage(int page);
+    Q_INVOKABLE QVector<QPolygonF> boundingPolygonsOnPage(int page);
 
-    QString searchString() const;
-    void setSearchString(QString searchString);
+    int currentPage() const { return m_currentPage; }
+    void setCurrentPage(int currentPage);
 
-    QVector<QPolygonF> matchGeometry() const;
+    int currentResult() const { return m_currentResult; }
+    void setCurrentResult(int currentResult);
+
+    QVector<QPolygonF> currentPageBoundingPolygons() const;
+    QVector<QPolygonF> currentResultBoundingPolygons() const;
 
 signals:
     void documentChanged();
-    void pageChanged();
-    void searchStringChanged();
-    void matchGeometryChanged();
+    void currentPageChanged();
+    void currentResultChanged();
+    void currentPageBoundingPolygonsChanged();
+    void currentResultBoundingPolygonsChanged();
 
 private:
     void updateResults();
+    void onResultsChanged();
 
 private:
     QQuickPdfDocument *m_quickDocument = nullptr;
-    QString m_searchString;
-    QVector<QPolygonF> m_matchGeometry;
-    int m_page;
+    int m_currentPage = 0;
+    int m_currentResult = 0;
+    bool m_suspendSignals = false;
 
     Q_DISABLE_COPY(QQuickPdfSearchModel)
 };