diff --git a/xapian-core/api/queryinternal.cc b/xapian-core/api/queryinternal.cc
index 8b3a4cda3566..c74edc84c5ab 100644
--- a/xapian-core/api/queryinternal.cc
+++ b/xapian-core/api/queryinternal.cc
@@ -134,6 +134,7 @@ struct CmpMaxOrTerms {
     }
 };
 
+// FIXME: Uses PostList::get_termfreq_*()...
 /// Comparison functor which orders by descending termfreq.
 struct ComparePostListTermFreqAscending {
     /// Order PostListAndTermFreq by descending tf.
@@ -149,6 +150,7 @@ struct ComparePostListTermFreqAscending {
     }
 };
 
+// FIXME: Uses PostList::get_termfreq_*()...
 template<typename T>
 class Context {
     /** Helper for initialisation when T = PostList*.
diff --git a/xapian-core/backends/contiguousalldocspostlist.cc b/xapian-core/backends/contiguousalldocspostlist.cc
index 28961d8f0a74..a0d581e0a587 100644
--- a/xapian-core/backends/contiguousalldocspostlist.cc
+++ b/xapian-core/backends/contiguousalldocspostlist.cc
@@ -92,6 +92,14 @@ ContiguousAllDocsPostList::at_end() const
     return did == 0;
 }
 
+void
+ContiguousAllDocsPostList::get_used_docid_range(Xapian::docid& first,
+						Xapian::docid& last) const
+{
+    first = did;
+    last = doccount;
+}
+
 string
 ContiguousAllDocsPostList::get_description() const
 {
diff --git a/xapian-core/backends/contiguousalldocspostlist.h b/xapian-core/backends/contiguousalldocspostlist.h
index 5a2d065f8a35..81885e9fd303 100644
--- a/xapian-core/backends/contiguousalldocspostlist.h
+++ b/xapian-core/backends/contiguousalldocspostlist.h
@@ -78,6 +78,8 @@ class ContiguousAllDocsPostList : public LeafPostList {
     /// Return true if and only if we're off the end of the list.
     bool at_end() const;
 
+    void get_used_docid_range(Xapian::docid& first, Xapian::docid& last) const;
+
     /// Return a string description of this object.
     std::string get_description() const;
 };
diff --git a/xapian-core/backends/glass/glass_postlist.cc b/xapian-core/backends/glass/glass_postlist.cc
index b490f3d081ff..bcaf2290de91 100644
--- a/xapian-core/backends/glass/glass_postlist.cc
+++ b/xapian-core/backends/glass/glass_postlist.cc
@@ -1020,6 +1020,22 @@ GlassPostList::jump_to(Xapian::docid desired_did)
     RETURN(desired_did == did);
 }
 
+void
+GlassPostList::get_used_docid_range(Xapian::docid& first,
+				    Xapian::docid& last) const
+{
+    if (pos == NULL) {
+	first = last = 1;
+    } else {
+	first = first_did_in_chunk;
+	if (is_last_chunk) {
+	    last = last_did_in_chunk;
+	} else {
+	    last = this_db->get_lastdocid();
+	}
+    }
+}
+
 string
 GlassPostList::get_description() const
 {
diff --git a/xapian-core/backends/glass/glass_postlist.h b/xapian-core/backends/glass/glass_postlist.h
index 4480d8aea627..f424bce692e4 100644
--- a/xapian-core/backends/glass/glass_postlist.h
+++ b/xapian-core/backends/glass/glass_postlist.h
@@ -291,6 +291,8 @@ class GlassPostList : public LeafPostList {
     /// Return true if and only if we're off the end of the list.
     bool at_end() const { return is_at_end; }
 
+    void get_used_docid_range(Xapian::docid& first, Xapian::docid& last) const;
+
     /// Get a description of the document.
     std::string get_description() const;
 
diff --git a/xapian-core/backends/honey/honey_alldocspostlist.cc b/xapian-core/backends/honey/honey_alldocspostlist.cc
index 4b4d0d23b3e2..735c5bbb9468 100644
--- a/xapian-core/backends/honey/honey_alldocspostlist.cc
+++ b/xapian-core/backends/honey/honey_alldocspostlist.cc
@@ -1,7 +1,7 @@
 /** @file honey_alldocspostlist.cc
  * @brief A PostList which iterates over all documents in a HoneyDatabase.
  */
-/* Copyright (C) 2006,2007,2008,2009,2018 Olly Betts
+/* Copyright (C) 2006,2007,2008,2009,2018,2019 Olly Betts
  * Copyright (C) 2008 Lemur Consulting Ltd
  *
  * This program is free software; you can redistribute it and/or modify
@@ -35,10 +35,12 @@ using namespace Honey;
 using namespace std;
 
 HoneyAllDocsPostList::HoneyAllDocsPostList(const HoneyDatabase* db,
-					   Xapian::doccount doccount_)
+					   Xapian::doccount doccount_,
+					   Xapian::docid last_docid_)
     : LeafPostList(string()),
       cursor(db->get_postlist_cursor()),
-      doccount(doccount_)
+      doccount(doccount_),
+      last_docid(last_docid_)
 {
     LOGCALL_CTOR(DB, "HoneyAllDocsPostList", db | doccount_);
     static const char doclen_key_prefix[2] = {
@@ -186,6 +188,16 @@ HoneyAllDocsPostList::check(Xapian::docid did, double, bool& valid)
     return NULL;
 }
 
+void
+HoneyAllDocsPostList::get_used_docid_range(Xapian::docid& first,
+					   Xapian::docid& last) const
+{
+    // We can't use reader.get_docid() as that's not set up yet.
+    // FIXME: Perhaps we should arrange that it is?
+    first = 1;
+    last = last_docid;
+}
+
 string
 HoneyAllDocsPostList::get_description() const
 {
@@ -193,6 +205,8 @@ HoneyAllDocsPostList::get_description() const
     desc += str(get_docid());
     desc += ",doccount=";
     desc += str(doccount);
+    desc += ",last_docid=";
+    desc += str(last_docid);
     desc += ')';
     return desc;
 }
diff --git a/xapian-core/backends/honey/honey_alldocspostlist.h b/xapian-core/backends/honey/honey_alldocspostlist.h
index a443a52f299b..28a66b056e23 100644
--- a/xapian-core/backends/honey/honey_alldocspostlist.h
+++ b/xapian-core/backends/honey/honey_alldocspostlist.h
@@ -1,7 +1,7 @@
 /** @file honey_alldocspostlist.h
  * @brief A PostList which iterates over all documents in a HoneyDatabase.
  */
-/* Copyright (C) 2006,2007,2008,2009,2017,2018 Olly Betts
+/* Copyright (C) 2006,2007,2008,2009,2017,2018,2019 Olly Betts
  * Copyright (C) 2008 Lemur Consulting Ltd
  *
  * This program is free software; you can redistribute it and/or modify
@@ -149,8 +149,13 @@ class HoneyAllDocsPostList : public LeafPostList {
     /// The number of documents in the database.
     Xapian::doccount doccount;
 
+    /// Upper bound on the highest used docid.
+    Xapian::docid last_docid;
+
   public:
-    HoneyAllDocsPostList(const HoneyDatabase* db_, Xapian::doccount doccount_);
+    HoneyAllDocsPostList(const HoneyDatabase* db_,
+			 Xapian::doccount doccount_,
+			 Xapian::docid last_docid_);
 
     ~HoneyAllDocsPostList();
 
@@ -170,6 +175,8 @@ class HoneyAllDocsPostList : public LeafPostList {
 
     PostList* check(Xapian::docid did, double w_min, bool& valid);
 
+    void get_used_docid_range(Xapian::docid& first, Xapian::docid& last) const;
+
     std::string get_description() const;
 };
 
diff --git a/xapian-core/backends/honey/honey_database.cc b/xapian-core/backends/honey/honey_database.cc
index 35dcba5919b9..866cf5ccddff 100644
--- a/xapian-core/backends/honey/honey_database.cc
+++ b/xapian-core/backends/honey/honey_database.cc
@@ -256,7 +256,7 @@ HoneyDatabase::open_leaf_post_list(const string& term, bool need_read_pos) const
 {
     if (term.empty()) {
 	Assert(!need_read_pos);
-	return new HoneyAllDocsPostList(this, get_doccount());
+	return new HoneyAllDocsPostList(this, get_doccount(), get_lastdocid());
     }
 
     return postlist_table.open_post_list(this, term, need_read_pos);
diff --git a/xapian-core/backends/honey/honey_postlist.cc b/xapian-core/backends/honey/honey_postlist.cc
index d8ef6ef14218..90fc946514d9 100644
--- a/xapian-core/backends/honey/honey_postlist.cc
+++ b/xapian-core/backends/honey/honey_postlist.cc
@@ -246,6 +246,21 @@ HoneyPostList::skip_to(Xapian::docid did, double)
     return NULL;
 }
 
+void
+HoneyPostList::get_used_docid_range(Xapian::docid& first,
+				    Xapian::docid& last) const
+{
+    Assert(!started);
+    if (reader.get_termfreq()) {
+	first = reader.get_docid();
+	last = last_did;
+	AssertRel(first,<=,last);
+    } else {
+	first = 1;
+	last = 0;
+    }
+}
+
 string
 HoneyPostList::get_description() const
 {
diff --git a/xapian-core/backends/honey/honey_postlist.h b/xapian-core/backends/honey/honey_postlist.h
index f2e783f4f8ea..7279abfdea29 100644
--- a/xapian-core/backends/honey/honey_postlist.h
+++ b/xapian-core/backends/honey/honey_postlist.h
@@ -203,6 +203,8 @@ class HoneyPostList : public LeafPostList {
 
     PostList* skip_to(Xapian::docid did, double w_min);
 
+    void get_used_docid_range(Xapian::docid& first, Xapian::docid& last) const;
+
     std::string get_description() const;
 };
 
diff --git a/xapian-core/backends/inmemory/inmemory_database.cc b/xapian-core/backends/inmemory/inmemory_database.cc
index 6991686423cc..4214c30dd5b6 100644
--- a/xapian-core/backends/inmemory/inmemory_database.cc
+++ b/xapian-core/backends/inmemory/inmemory_database.cc
@@ -148,6 +148,19 @@ InMemoryPostList::at_end() const
     return (pos == end);
 }
 
+void
+InMemoryPostList::get_used_docid_range(Xapian::docid& first,
+				       Xapian::docid& last) const
+{
+    Assert(!started);
+    if (pos == end) {
+	first = last = 1;
+    } else {
+	first = pos->did;
+	last = (end - 1)->did;
+    }
+}
+
 string
 InMemoryPostList::get_description() const
 {
@@ -364,6 +377,14 @@ InMemoryAllDocsPostList::at_end() const
     return (did > db->termlists.size());
 }
 
+void
+InMemoryAllDocsPostList::get_used_docid_range(Xapian::docid& first,
+					      Xapian::docid& last) const
+{
+    first = 1;
+    last = db->termlists.size();
+}
+
 string
 InMemoryAllDocsPostList::get_description() const
 {
diff --git a/xapian-core/backends/inmemory/inmemory_database.h b/xapian-core/backends/inmemory/inmemory_database.h
index d7516e234d98..e4bb64a79459 100644
--- a/xapian-core/backends/inmemory/inmemory_database.h
+++ b/xapian-core/backends/inmemory/inmemory_database.h
@@ -173,6 +173,8 @@ class InMemoryPostList : public LeafPostList {
     // True if we're off the end of the list.
     bool at_end() const;
 
+    void get_used_docid_range(Xapian::docid& first, Xapian::docid& last) const;
+
     string get_description() const;
 };
 
@@ -207,6 +209,8 @@ class InMemoryAllDocsPostList : public LeafPostList {
     // True if we're off the end of the list
     bool at_end() const;
 
+    void get_used_docid_range(Xapian::docid& first, Xapian::docid& last) const;
+
     string get_description() const;
 };
 
diff --git a/xapian-core/backends/multi/multi_postlist.cc b/xapian-core/backends/multi/multi_postlist.cc
index 20162b060914..5c5eca7574e0 100644
--- a/xapian-core/backends/multi/multi_postlist.cc
+++ b/xapian-core/backends/multi/multi_postlist.cc
@@ -205,6 +205,14 @@ MultiPostList::skip_to(Xapian::docid did, double w_min)
     return NULL;
 }
 
+void
+MultiPostList::get_used_docid_range(Xapian::docid&, Xapian::docid&) const
+{
+    // This is only used during the match and should never get called on a
+    // MultiPostList object.
+    Assert(false);
+}
+
 std::string
 MultiPostList::get_description() const
 {
diff --git a/xapian-core/backends/multi/multi_postlist.h b/xapian-core/backends/multi/multi_postlist.h
index 3fbbbba571a9..f1e3e1e8fb2c 100644
--- a/xapian-core/backends/multi/multi_postlist.h
+++ b/xapian-core/backends/multi/multi_postlist.h
@@ -133,6 +133,8 @@ class MultiPostList : public PostList {
     // which subdatabase a given docid will be in, and so we only actually need
     // to call check() on that subdatabase.
 
+    void get_used_docid_range(Xapian::docid& first, Xapian::docid& last) const;
+
     /// Return a string description of this object.
     std::string get_description() const;
 };
diff --git a/xapian-core/backends/postlist.h b/xapian-core/backends/postlist.h
index 6e8157b77f36..07ee1b0fe0ae 100644
--- a/xapian-core/backends/postlist.h
+++ b/xapian-core/backends/postlist.h
@@ -188,6 +188,8 @@ class PostList {
     /// Gather PositionList* objects for a subtree.
     virtual void gather_position_lists(OrPositionList* orposlist);
 
+    virtual void get_used_docid_range(docid& first, docid& last) const = 0;
+
     /// Return a string description of this object.
     virtual std::string get_description() const = 0;
 };
diff --git a/xapian-core/backends/remote/net_postlist.cc b/xapian-core/backends/remote/net_postlist.cc
index 6d379836d2ef..ae7f9b0c8620 100644
--- a/xapian-core/backends/remote/net_postlist.cc
+++ b/xapian-core/backends/remote/net_postlist.cc
@@ -22,6 +22,7 @@
 
 #include <config.h>
 
+#include "assert.h"
 #include "net_postlist.h"
 #include "pack.h"
 #include "unicode/description_append.h"
@@ -99,6 +100,14 @@ NetworkPostList::at_end() const
     return (pos == NULL && started);
 }
 
+void
+NetworkPostList::get_used_docid_range(Xapian::docid&, Xapian::docid&) const
+{
+    // This is only used during the match and should never get called on a
+    // NetworkPostList object.
+    Assert(false);
+}
+
 string
 NetworkPostList::get_description() const
 {
diff --git a/xapian-core/backends/remote/net_postlist.h b/xapian-core/backends/remote/net_postlist.h
index b98def512ba6..86490627481b 100644
--- a/xapian-core/backends/remote/net_postlist.h
+++ b/xapian-core/backends/remote/net_postlist.h
@@ -86,6 +86,8 @@ class NetworkPostList : public LeafPostList {
     /// Return true if and only if we've moved off the end of the list.
     bool at_end() const;
 
+    void get_used_docid_range(Xapian::docid& first, Xapian::docid& last) const;
+
     /// Get a description of the postlist.
     string get_description() const;
 };
diff --git a/xapian-core/matcher/andmaybepostlist.h b/xapian-core/matcher/andmaybepostlist.h
index 401b9dbff810..31027271567c 100644
--- a/xapian-core/matcher/andmaybepostlist.h
+++ b/xapian-core/matcher/andmaybepostlist.h
@@ -93,6 +93,8 @@ class AndMaybePostList : public WrapperPostList {
 
     PostList* check(Xapian::docid did, double w_min, bool& valid);
 
+    // FIXME: If ranges are disjoint, the RHS can be dropped.
+
     std::string get_description() const;
 
     Xapian::termcount get_wdf() const;
diff --git a/xapian-core/matcher/andnotpostlist.h b/xapian-core/matcher/andnotpostlist.h
index 9fa4e2e274f2..51b42dd49539 100644
--- a/xapian-core/matcher/andnotpostlist.h
+++ b/xapian-core/matcher/andnotpostlist.h
@@ -59,6 +59,8 @@ class AndNotPostList : public WrapperPostList {
 
     PostList* check(Xapian::docid did, double w_min, bool& valid);
 
+    // FIXME: If ranges are disjoint, the "NOT" can be dropped.
+
     std::string get_description() const;
 };
 
diff --git a/xapian-core/matcher/boolorpostlist.cc b/xapian-core/matcher/boolorpostlist.cc
index 4dfc6129b0f9..acb8c633293f 100644
--- a/xapian-core/matcher/boolorpostlist.cc
+++ b/xapian-core/matcher/boolorpostlist.cc
@@ -243,6 +243,19 @@ BoolOrPostList::at_end() const
     return false;
 }
 
+void
+BoolOrPostList::get_used_docid_range(Xapian::docid& first,
+				     Xapian::docid& last) const
+{
+    plist[0].pl->get_used_docid_range(first, last);
+    for (size_t i = 1; i != n_kids; ++i) {
+	Xapian::docid f, l;
+	plist[i].pl->get_used_docid_range(f, l);
+	first = min(first, f);
+	last = max(last, l);
+    }
+}
+
 std::string
 BoolOrPostList::get_description() const
 {
diff --git a/xapian-core/matcher/boolorpostlist.h b/xapian-core/matcher/boolorpostlist.h
index 9e2850a8712a..43d7a8753490 100644
--- a/xapian-core/matcher/boolorpostlist.h
+++ b/xapian-core/matcher/boolorpostlist.h
@@ -143,6 +143,8 @@ class BoolOrPostList : public PostList {
 
     PostList* skip_to(Xapian::docid did, double w_min);
 
+    void get_used_docid_range(Xapian::docid& first, Xapian::docid& last) const;
+
     std::string get_description() const;
 
     Xapian::termcount get_wdf() const;
diff --git a/xapian-core/matcher/externalpostlist.cc b/xapian-core/matcher/externalpostlist.cc
index 1a8ee523bf41..5e054c8d0afb 100644
--- a/xapian-core/matcher/externalpostlist.cc
+++ b/xapian-core/matcher/externalpostlist.cc
@@ -52,6 +52,7 @@ ExternalPostList::ExternalPostList(const Xapian::Database& db,
     }
     source->set_max_weight_cached_flag_ptr_(max_weight_cached_flag_ptr);
     source->reset(db, shard_index);
+    db.internal->get_used_docid_range(first_docid, last_docid);
 }
 
 Xapian::doccount
@@ -173,6 +174,14 @@ ExternalPostList::count_matching_subqs() const
     return 1;
 }
 
+void
+ExternalPostList::get_used_docid_range(Xapian::docid& first,
+				       Xapian::docid& last) const
+{
+    first = first_docid;
+    last = last_docid;
+}
+
 string
 ExternalPostList::get_description() const
 {
diff --git a/xapian-core/matcher/externalpostlist.h b/xapian-core/matcher/externalpostlist.h
index c26ec16efe97..06e744499ec3 100644
--- a/xapian-core/matcher/externalpostlist.h
+++ b/xapian-core/matcher/externalpostlist.h
@@ -43,6 +43,8 @@ class ExternalPostList : public PostList {
 
     double factor;
 
+    Xapian::docid first_docid, last_docid;
+
     PostList * update_after_advance();
 
   public:
@@ -82,6 +84,8 @@ class ExternalPostList : public PostList {
 
     Xapian::termcount count_matching_subqs() const;
 
+    void get_used_docid_range(Xapian::docid& first, Xapian::docid& last) const;
+
     std::string get_description() const;
 };
 
diff --git a/xapian-core/matcher/localsubmatch.cc b/xapian-core/matcher/localsubmatch.cc
index 5a298bae0a0f..36a4dcb6be63 100644
--- a/xapian-core/matcher/localsubmatch.cc
+++ b/xapian-core/matcher/localsubmatch.cc
@@ -196,6 +196,7 @@ LocalSubMatch::get_postlist(PostListTree * matcher,
     RETURN(pl);
 }
 
+// Uses PostList::get_termfreq_*()...
 PostList *
 LocalSubMatch::make_synonym_postlist(PostListTree* pltree,
 				     PostList* or_pl,
diff --git a/xapian-core/matcher/maxpostlist.cc b/xapian-core/matcher/maxpostlist.cc
index e97555fcb88c..4855530e829b 100644
--- a/xapian-core/matcher/maxpostlist.cc
+++ b/xapian-core/matcher/maxpostlist.cc
@@ -242,6 +242,19 @@ MaxPostList::skip_to(Xapian::docid did_min, double w_min)
     return NULL;
 }
 
+void
+MaxPostList::get_used_docid_range(Xapian::docid& first,
+				  Xapian::docid& last) const
+{
+    plist[0]->get_used_docid_range(first, last);
+    for (size_t i = 1; i != n_kids; ++i) {
+	Xapian::docid f, l;
+	plist[i]->get_used_docid_range(f, l);
+	first = min(first, f);
+	last = max(last, l);
+    }
+}
+
 string
 MaxPostList::get_description() const
 {
diff --git a/xapian-core/matcher/maxpostlist.h b/xapian-core/matcher/maxpostlist.h
index aed90acd7383..c5887da4f5e0 100644
--- a/xapian-core/matcher/maxpostlist.h
+++ b/xapian-core/matcher/maxpostlist.h
@@ -103,6 +103,8 @@ class MaxPostList : public PostList {
 
     PostList* skip_to(Xapian::docid, double w_min);
 
+    void get_used_docid_range(Xapian::docid& first, Xapian::docid& last) const;
+
     std::string get_description() const;
 
     /** get_wdf() for MaxPostlist returns the sum of the wdfs of the
diff --git a/xapian-core/matcher/multiandpostlist.cc b/xapian-core/matcher/multiandpostlist.cc
index f27d156630be..efa9a5b04c10 100644
--- a/xapian-core/matcher/multiandpostlist.cc
+++ b/xapian-core/matcher/multiandpostlist.cc
@@ -94,14 +94,20 @@ MultiAndPostList::get_termfreq_est() const
     LOGCALL(MATCH, Xapian::doccount, "MultiAndPostList::get_termfreq_est", NO_ARGS);
     if (rare(db_size == 0))
 	RETURN(0);
-    // We calculate the estimate assuming independence.  With this assumption,
-    // the estimate is the product of the estimates for the sub-postlists
-    // divided by db_size (n_kids - 1) times.
-    double result = plist[0]->get_termfreq_est();
-    for (size_t i = 1; i < n_kids; ++i) {
-	result = (result * plist[i]->get_termfreq_est()) / db_size;
+    Xapian::docid first, last;
+    MultiAndPostList::get_used_docid_range(first, last);
+    if (last - first + 1 == 0)
+	RETURN(0);
+    // We calculate the estimate assuming independence.
+    double r = (last - first + 1);
+    for (size_t i = 0; i < n_kids; ++i) {
+	auto est = plist[i]->get_termfreq_est();
+	plist[i]->get_used_docid_range(first, last);
+	if (last - first + 1 == 0)
+	    RETURN(0);
+	r *= double(est) / (last - first + 1);
     }
-    return static_cast<Xapian::doccount>(result + 0.5);
+    RETURN(static_cast<Xapian::doccount>(r + 0.5));
 }
 
 TermFreqs
@@ -221,6 +227,20 @@ MultiAndPostList::skip_to(Xapian::docid did_min, double w_min)
     return find_next_match(w_min);
 }
 
+void
+MultiAndPostList::get_used_docid_range(Xapian::docid& first,
+				       Xapian::docid& last) const
+{
+    plist[0]->get_used_docid_range(first, last);
+    for (size_t i = 1; i != n_kids; ++i) {
+	Xapian::docid f, l;
+	plist[i]->get_used_docid_range(f, l);
+	first = max(first, f);
+	last = min(last, l);
+	if (last < first) break;
+    }
+}
+
 std::string
 MultiAndPostList::get_description() const
 {
diff --git a/xapian-core/matcher/multiandpostlist.h b/xapian-core/matcher/multiandpostlist.h
index b358cfe8152c..ed64ed3388b8 100644
--- a/xapian-core/matcher/multiandpostlist.h
+++ b/xapian-core/matcher/multiandpostlist.h
@@ -176,6 +176,8 @@ class MultiAndPostList : public PostList {
 
     PostList* skip_to(Xapian::docid, double w_min);
 
+    void get_used_docid_range(Xapian::docid& first, Xapian::docid& last) const;
+
     std::string get_description() const;
 
     /** get_wdf() for MultiAndPostlists returns the sum of the wdfs of the
diff --git a/xapian-core/matcher/multixorpostlist.cc b/xapian-core/matcher/multixorpostlist.cc
index 8342ad4b18e8..716a0746e93d 100644
--- a/xapian-core/matcher/multixorpostlist.cc
+++ b/xapian-core/matcher/multixorpostlist.cc
@@ -322,6 +322,19 @@ MultiXorPostList::skip_to(Xapian::docid did_min, double w_min)
     RETURN(next(w_min));
 }
 
+void
+MultiXorPostList::get_used_docid_range(Xapian::docid& first,
+				       Xapian::docid& last) const
+{
+    plist[0]->get_used_docid_range(first, last);
+    for (size_t i = 1; i != n_kids; ++i) {
+	Xapian::docid f, l;
+	plist[i]->get_used_docid_range(f, l);
+	first = min(first, f);
+	last = max(last, l);
+    }
+}
+
 string
 MultiXorPostList::get_description() const
 {
diff --git a/xapian-core/matcher/multixorpostlist.h b/xapian-core/matcher/multixorpostlist.h
index de4f4050425b..c3638d586ade 100644
--- a/xapian-core/matcher/multixorpostlist.h
+++ b/xapian-core/matcher/multixorpostlist.h
@@ -102,6 +102,8 @@ class MultiXorPostList : public PostList {
 
     PostList* skip_to(Xapian::docid, double w_min);
 
+    void get_used_docid_range(Xapian::docid& first, Xapian::docid& last) const;
+
     std::string get_description() const;
 
     /** get_wdf() for MultiXorPostlists returns the sum of the wdfs of the
diff --git a/xapian-core/matcher/orpostlist.cc b/xapian-core/matcher/orpostlist.cc
index 5f220b93bec1..6ab8425df662 100644
--- a/xapian-core/matcher/orpostlist.cc
+++ b/xapian-core/matcher/orpostlist.cc
@@ -373,13 +373,73 @@ estimate_or_assuming_indep(double a, double b, double n, T& res)
     }
 }
 
+template<typename T>
+static void
+estimate_or_assuming_indep(double a, double af, double al,
+			   double b, double bf, double bl,
+			   double n, T& res)
+{
+    if (rare(n == 0.0)) {
+	res = 0;
+	return;
+    }
+
+    // Clamp estimates to range lengths.  FIXME: Needed?
+    a = min(a, al - af + 1.0);
+    b = min(b, bl - bf + 1.0);
+    AssertRel(a,>=,0);
+    AssertRel(b,>=,0);
+
+    if (al < bf || bl < af) {
+	// Disjoint ranges.
+	res = a + b;
+	return;
+    }
+
+    // Arrange for af <= bf.
+    if (af > bf) {
+	swap(a, b);
+	swap(af, bf);
+	swap(al, bl);
+    }
+
+    // Arrange for al <= bl.
+    if (al > bl) {
+	bf += (al - bl);
+	bl = al;
+    }
+
+    double arate = a / (al - af + 1);
+    double brate = b / (bl - bf + 1);
+
+    double r = arate * (bf - af) +
+	       brate * (bl - al) +
+	       (arate + brate - arate * brate) * (al - bf + 1);
+    res = static_cast<T>(r + 0.5);
+}
+
 Xapian::doccount
 OrPostList::get_termfreq_est() const
 {
     auto l_tf_est = l->get_termfreq_est();
     auto r_tf_est = r->get_termfreq_est();
+    Xapian::docid l_first, l_last, r_first, r_last;
+    l->get_used_docid_range(l_first, l_last);
+    r->get_used_docid_range(r_first, r_last);
     Xapian::doccount tf_est;
-    estimate_or_assuming_indep(l_tf_est, r_tf_est, db_size, tf_est);
+    if (l_last) {
+	AssertRel(l_first,<=,l_last);
+    } else {
+	AssertEq(l_first, 1);
+    }
+    if (r_last) {
+	AssertRel(r_first,<=,r_last);
+    } else {
+	AssertEq(r_first, 1);
+    }
+    estimate_or_assuming_indep(l_tf_est, l_first, l_last,
+			       r_tf_est, r_first, r_last,
+			       db_size, tf_est);
     return tf_est;
 }
 
@@ -426,6 +486,17 @@ OrPostList::at_end() const
     return false;
 }
 
+void
+OrPostList::get_used_docid_range(Xapian::docid& first,
+				 Xapian::docid& last) const
+{
+    l->get_used_docid_range(first, last);
+    Xapian::docid first2, last2;
+    r->get_used_docid_range(first2, last2);
+    first = min(first, first2);
+    last = max(last, last2);
+}
+
 std::string
 OrPostList::get_description() const
 {
diff --git a/xapian-core/matcher/orpostlist.h b/xapian-core/matcher/orpostlist.h
index bf03c149aa20..349f36530d79 100644
--- a/xapian-core/matcher/orpostlist.h
+++ b/xapian-core/matcher/orpostlist.h
@@ -102,6 +102,8 @@ class OrPostList : public PostList {
 
     PostList* check(Xapian::docid did, double w_min, bool& valid);
 
+    void get_used_docid_range(Xapian::docid& first, Xapian::docid& last) const;
+
     std::string get_description() const;
 
     Xapian::termcount get_wdf() const;
diff --git a/xapian-core/matcher/postlisttree.h b/xapian-core/matcher/postlisttree.h
index 42ab9e5f88de..aa295f940bbe 100644
--- a/xapian-core/matcher/postlisttree.h
+++ b/xapian-core/matcher/postlisttree.h
@@ -126,17 +126,33 @@ class PostListTree {
 
     Xapian::doccount get_termfreq_max() const {
 	Xapian::doccount result = 0;
-	for (Xapian::doccount i = 0; i != n_shards; ++i)
-	    if (shard_pls[i])
-		result += shard_pls[i]->get_termfreq_max();
+	for (Xapian::doccount i = 0; i != n_shards; ++i) {
+	    if (shard_pls[i]) {
+		Xapian::doccount tf_max = shard_pls[i]->get_termfreq_max();
+		Xapian::docid first, last;
+		shard_pls[i]->get_used_docid_range(first, last);
+		if (last >= first) {
+		    Xapian::doccount n_docs = last - first + 1;
+		    result += min(n_docs, tf_max);
+		}
+	    }
+	}
 	return result;
     }
 
     Xapian::doccount get_termfreq_est() const {
 	Xapian::doccount result = 0;
-	for (Xapian::doccount i = 0; i != n_shards; ++i)
-	    if (shard_pls[i])
-		result += shard_pls[i]->get_termfreq_est();
+	for (Xapian::doccount i = 0; i != n_shards; ++i) {
+	    if (shard_pls[i]) {
+		Xapian::doccount tf_est = shard_pls[i]->get_termfreq_est();
+		Xapian::docid first, last;
+		shard_pls[i]->get_used_docid_range(first, last);
+		if (last >= first) {
+		    Xapian::doccount n_docs = last - first + 1;
+		    result += min(n_docs, tf_est);
+		}
+	    }
+	}
 	return result;
     }
 
diff --git a/xapian-core/matcher/valuerangepostlist.cc b/xapian-core/matcher/valuerangepostlist.cc
index 4fe42ee9ed2d..1f62c5146588 100644
--- a/xapian-core/matcher/valuerangepostlist.cc
+++ b/xapian-core/matcher/valuerangepostlist.cc
@@ -238,6 +238,14 @@ ValueRangePostList::count_matching_subqs() const
     return 1;
 }
 
+void
+ValueRangePostList::get_used_docid_range(Xapian::docid& first,
+					 Xapian::docid& last) const
+{
+    // FIXME: Can we do better?
+    db->get_used_docid_range(first, last);
+}
+
 string
 ValueRangePostList::get_description() const
 {
diff --git a/xapian-core/matcher/valuerangepostlist.h b/xapian-core/matcher/valuerangepostlist.h
index 178d7a872501..a9262d511ce7 100644
--- a/xapian-core/matcher/valuerangepostlist.h
+++ b/xapian-core/matcher/valuerangepostlist.h
@@ -82,6 +82,8 @@ class ValueRangePostList : public PostList {
 
     Xapian::termcount count_matching_subqs() const;
 
+    void get_used_docid_range(Xapian::docid& first, Xapian::docid& last) const;
+
     std::string get_description() const;
 };
 
diff --git a/xapian-core/matcher/wrapperpostlist.cc b/xapian-core/matcher/wrapperpostlist.cc
index fb753fcb3981..a94685a4d60c 100644
--- a/xapian-core/matcher/wrapperpostlist.cc
+++ b/xapian-core/matcher/wrapperpostlist.cc
@@ -100,6 +100,13 @@ WrapperPostList::skip_to(Xapian::docid did, double w_min)
     return NULL;
 }
 
+void
+WrapperPostList::get_used_docid_range(Xapian::docid& first,
+				      Xapian::docid& last) const
+{
+    pl->get_used_docid_range(first, last);
+}
+
 std::string
 WrapperPostList::get_description() const
 {
diff --git a/xapian-core/matcher/wrapperpostlist.h b/xapian-core/matcher/wrapperpostlist.h
index 310566a11b6c..3591c8eef766 100644
--- a/xapian-core/matcher/wrapperpostlist.h
+++ b/xapian-core/matcher/wrapperpostlist.h
@@ -68,6 +68,8 @@ class WrapperPostList : public PostList {
 
     PostList* skip_to(Xapian::docid, double w_min);
 
+    void get_used_docid_range(Xapian::docid& first, Xapian::docid& last) const;
+
     std::string get_description() const;
 
     Xapian::termcount get_wdf() const;
diff --git a/xapian-core/tests/api_anydb.cc b/xapian-core/tests/api_anydb.cc
index 63298b1ddd89..3645a1f62573 100644
--- a/xapian-core/tests/api_anydb.cc
+++ b/xapian-core/tests/api_anydb.cc
@@ -1597,7 +1597,6 @@ DEFINE_TESTCASE(msetzeroitems1, backend) {
 // test that the matches_* of a simple query are as expected
 DEFINE_TESTCASE(matches1, backend) {
     bool multi = startswith(get_dbtype(), "multi");
-    bool remote = get_dbtype().find("remote") != string::npos;
 
     Xapian::Enquire enquire(get_database("apitest_simpledata"));
     Xapian::Query myquery;
@@ -1653,10 +1652,10 @@ DEFINE_TESTCASE(matches1, backend) {
 	TEST_EQUAL(mymset.get_matches_lower_bound(), 0);
     }
     TEST_REL(mymset.get_matches_lower_bound(),<=,mymset.get_matches_estimated());
-    TEST_EQUAL(mymset.get_matches_estimated(), 1);
+    TEST_EQUAL(mymset.get_matches_estimated(), 2);
     TEST_EQUAL(mymset.get_matches_upper_bound(), 2);
     TEST_REL(mymset.get_uncollapsed_matches_lower_bound(),<=,mymset.get_uncollapsed_matches_estimated());
-    TEST_EQUAL(mymset.get_uncollapsed_matches_estimated(), 1);
+    TEST_EQUAL(mymset.get_uncollapsed_matches_estimated(), 2);
     TEST_EQUAL(mymset.get_uncollapsed_matches_upper_bound(), 2);
 
     mymset = enquire.get_mset(0, 1);
@@ -1679,27 +1678,19 @@ DEFINE_TESTCASE(matches1, backend) {
     enquire.set_query(myquery);
     mymset = enquire.get_mset(0, 0);
     TEST_EQUAL(mymset.get_matches_lower_bound(), 1);
-    TEST_EQUAL(mymset.get_matches_estimated(), 2);
-    TEST_EQUAL(mymset.get_matches_upper_bound(), 2);
+    TEST_EQUAL(mymset.get_matches_estimated(), 1);
+    TEST_EQUAL(mymset.get_matches_upper_bound(), 1);
     TEST_EQUAL(mymset.get_uncollapsed_matches_lower_bound(), 1);
-    TEST_EQUAL(mymset.get_uncollapsed_matches_estimated(), 2);
-    TEST_EQUAL(mymset.get_uncollapsed_matches_upper_bound(), 2);
+    TEST_EQUAL(mymset.get_uncollapsed_matches_estimated(), 1);
+    TEST_EQUAL(mymset.get_uncollapsed_matches_upper_bound(), 1);
 
     mymset = enquire.get_mset(0, 1);
     TEST_EQUAL(mymset.get_matches_lower_bound(), 1);
+    TEST_EQUAL(mymset.get_matches_estimated(), 1);
+    TEST_EQUAL(mymset.get_matches_upper_bound(), 1);
     TEST_EQUAL(mymset.get_uncollapsed_matches_lower_bound(), 1);
-    if (multi && remote) {
-	// The matcher can tell there's only one match in this case.
-	TEST_EQUAL(mymset.get_matches_estimated(), 1);
-	TEST_EQUAL(mymset.get_uncollapsed_matches_estimated(), 1);
-	TEST_EQUAL(mymset.get_matches_upper_bound(), 1);
-	TEST_EQUAL(mymset.get_uncollapsed_matches_upper_bound(), 1);
-    } else {
-	TEST_EQUAL(mymset.get_matches_estimated(), 2);
-	TEST_EQUAL(mymset.get_uncollapsed_matches_estimated(), 2);
-	TEST_EQUAL(mymset.get_matches_upper_bound(), 2);
-	TEST_EQUAL(mymset.get_uncollapsed_matches_upper_bound(), 2);
-    }
+    TEST_EQUAL(mymset.get_uncollapsed_matches_estimated(), 1);
+    TEST_EQUAL(mymset.get_uncollapsed_matches_upper_bound(), 1);
 
     mymset = enquire.get_mset(0, 2);
     TEST_EQUAL(mymset.get_matches_lower_bound(), 1);
