commit bfcef51795c034d31fe3cd6595484f176c4d4b4a
Author: Olly Betts <olly@survex.com>
Date:   Mon Feb 23 13:21:22 2026 +1300

    WIP

diff --git a/xapian-core/api/database.cc b/xapian-core/api/database.cc
index 5e36a50dfc6d..1cc7d0de1671 100644
--- a/xapian-core/api/database.cc
+++ b/xapian-core/api/database.cc
@@ -196,6 +196,14 @@ Database::postlist_begin(string_view term) const
     return PostingIterator(new PostingIterator::Internal(pl, *this));
 }
 
+PostingIterator
+Database::postlist_begin(const Xapian::Query& query, bool prestart) const
+{
+    PostList* pl = internal->open_post_list(query, prestart);
+    if (!pl) return PostingIterator();
+    return PostingIterator(new PostingIterator::Internal(pl, *this));
+}
+
 TermIterator
 Database::termlist_begin(Xapian::docid did) const
 {
diff --git a/xapian-core/api/postingiteratorinternal.h b/xapian-core/api/postingiteratorinternal.h
index e85020d623e4..a8a44501be16 100644
--- a/xapian-core/api/postingiteratorinternal.h
+++ b/xapian-core/api/postingiteratorinternal.h
@@ -69,12 +69,20 @@ class PostingIterator::Internal {
     }
 
     bool next() {
-	(void)pl->next();
+	auto prune = pl->next();
+	if (rare(prune)) {
+	    delete pl;
+	    pl = prune;
+	}
 	return !pl->at_end();
     }
 
     bool skip_to(Xapian::docid did) {
-	(void)pl->skip_to(did);
+	auto prune = pl->skip_to(did);
+	if (rare(prune)) {
+	    delete pl;
+	    pl = prune;
+	}
 	return !pl->at_end();
     }
 
diff --git a/xapian-core/backends/databaseinternal.cc b/xapian-core/backends/databaseinternal.cc
index 43bc9970f219..e5b608c0e0ff 100644
--- a/xapian-core/backends/databaseinternal.cc
+++ b/xapian-core/backends/databaseinternal.cc
@@ -25,6 +25,8 @@
 
 #include "api/termlist.h"
 #include "heap.h"
+#include "matcher/localsubmatch.h"
+#include "matcher/postlisttree.h"
 #include "omassert.h"
 #include "postlist.h"
 #include "slowvaluelist.h"
@@ -77,6 +79,34 @@ Database::Internal::get_unique_terms_upper_bound() const
     return get_doclength_upper_bound();
 }
 
+PostList*
+Database::Internal::open_post_list(const Query& query, bool prestart) const
+{
+    // FIXME: query_length wtscheme
+    Xapian::termcount query_length = query.get_length();
+    // FIXME: Move to Enquire?
+    // FIXME: Also need special implementations for multi and remote (or UnimplementedError)
+    BoolWeight wtscheme;
+    LocalSubMatch localsubmatch(this, query, query_length, wtscheme, 0);
+    Xapian::Database db(const_cast<Database::Internal*>(this));
+
+    // FIXME: Need to arrange for these to be deleted.
+    ValueStreamDocument* vsdoc = new ValueStreamDocument{db};
+    PostListTree* pltree = new PostListTree(*vsdoc, db, wtscheme);
+
+    Xapian::termcount total_subqs_i = 0;
+    auto plest = localsubmatch.get_postlist(pltree, &total_subqs_i);
+    PostList* pl = plest.pl;
+    if (!prestart) {
+	auto prune = pl->next();
+	if (rare(prune)) {
+	    delete pl;
+	    pl = prune;
+	}
+    }
+    return pl;
+}
+
 // Discard any exceptions - we're called from the destructors of derived
 // classes so we can't safely throw.
 void
diff --git a/xapian-core/backends/databaseinternal.h b/xapian-core/backends/databaseinternal.h
index 67d95f502cd7..e36da3c4b9ca 100644
--- a/xapian-core/backends/databaseinternal.h
+++ b/xapian-core/backends/databaseinternal.h
@@ -218,6 +218,8 @@ class Database::Internal : public Xapian::Internal::intrusive_base {
     /** Return a PostList suitable for use in a PostingIterator. */
     virtual PostList* open_post_list(std::string_view term) const = 0;
 
+    /*virtual*/ PostList* open_post_list(const Query& query, bool prestart) const;
+
     /** Create a LeafPostList for use during a match.
      *
      *  @param term		The term to open a postlist for, or the empty
diff --git a/xapian-core/include/xapian/database.h b/xapian-core/include/xapian/database.h
index 2a15fe6b9283..3e2c4f626e3a 100644
--- a/xapian-core/include/xapian/database.h
+++ b/xapian-core/include/xapian/database.h
@@ -47,6 +47,7 @@ namespace Xapian {
 
 class Compactor;
 class Document;
+class Query;
 class WritableDatabase;
 
 /** An indexed database of documents.
@@ -260,6 +261,39 @@ class XAPIAN_VISIBILITY_DEFAULT Database {
 	return PostingIterator();
     }
 
+    PostingIterator postlist_begin(const std::string& term) const {
+	return postlist_begin(std::string_view{term});
+    }
+
+    /** End iterator corresponding to postlist_begin(). */
+    PostingIterator postlist_end(const std::string&) const noexcept {
+	return PostingIterator();
+    }
+
+    /** Start iterating the postings of a Query object.
+     *
+     *  This returns the document ids matching a query.  The key difference
+     *  compared to Enquire::get_mset() are that it can only return in
+     *  ascending document id order.  However get_mset() needs O(n) memory
+     *  so this can be useful if you want to process a lot of results.
+     *
+     *  @param query	The query to iterate the postings from.
+     *  @param prestart	If true the returned iterator needs incrementing to
+     *			be at the first result.  This is mainly useful as
+     *			you can call get_description() and see the results
+     *			of Xapian's query optimisation.  (Default: false)
+     *
+     *  @since Added in 1.5.0.
+     */
+    PostingIterator postlist_begin(const Xapian::Query& query,
+				   bool prestart = false) const;
+
+    /** End iterator corresponding to postlist_begin(). */
+    PostingIterator postlist_end(const Xapian::Query&,
+				 bool = false) const noexcept {
+	return PostingIterator();
+    }
+
     /** Start iterating the terms in a document.
      *
      *  @param did	The document id to iterate terms from
