diff --git a/NEWS b/NEWS
index fdf301876..284dabd5a 100644
--- a/NEWS
+++ b/NEWS
@@ -50,6 +50,10 @@ ver 0.21 (not yet released)
 * use GTest instead of cppunit
 
 ver 0.20.22 (not yet released)
+* protocol
+  - add tag fallbacks for AlbumArtistSort, ArtistSort
+  - "count group ..." can print an empty group
+  - fix broken command "list ... group"
 * storage
   - curl: URL-encode paths
 * Android
diff --git a/doc/protocol.rst b/doc/protocol.rst
index acc2d3c47..618e784ec 100644
--- a/doc/protocol.rst
+++ b/doc/protocol.rst
@@ -732,6 +732,10 @@ The music database
      count group artist
      count title Echoes group artist
 
+    A group with an empty value contains counts of matching song which
+    don't this group tag.  It exists only if at least one such song is
+    found.
+
 .. _command_find:
 
 :command:`find {FILTER} [sort {TYPE}] [window {START:END}]`
diff --git a/python/build/libs.py b/python/build/libs.py
index e4f621daf..025d933df 100644
--- a/python/build/libs.py
+++ b/python/build/libs.py
@@ -9,8 +9,8 @@ from build.ffmpeg import FfmpegProject
 from build.boost import BoostProject
 
 libmpdclient = MesonProject(
-    'https://www.musicpd.org/download/libmpdclient/2/libmpdclient-2.14.tar.xz',
-    '0a84e2791bfe3077cf22ee1784c805d5bb550803dffe56a39aa3690a38061372',
+    'https://www.musicpd.org/download/libmpdclient/2/libmpdclient-2.16.tar.xz',
+    'fa6bdab67c0e0490302b38f00c27b4959735c3ec8aef7a88327adb1407654464',
     'lib/libmpdclient.a',
 )
 
@@ -38,8 +38,8 @@ libvorbis = AutotoolsProject(
 )
 
 opus = AutotoolsProject(
-    'https://archive.mozilla.org/pub/opus/opus-1.2.1.tar.gz',
-    'cfafd339ccd9c5ef8d6ab15d7e1a412c054bf4cb4ecbbbcc78c12ef2def70732',
+    'https://archive.mozilla.org/pub/opus/opus-1.3.tar.gz',
+    '4f3d69aefdf2dbaf9825408e452a8a414ffc60494c70633560700398820dc550',
     'lib/libopus.a',
     [
         '--disable-shared', '--enable-static',
@@ -341,8 +341,8 @@ ffmpeg = FfmpegProject(
 )
 
 curl = AutotoolsProject(
-    'http://curl.haxx.se/download/curl-7.61.0.tar.xz',
-    'ef6e55192d04713673b4409ccbcb4cb6cd723137d6e10ca45b0c593a454e1720',
+    'http://curl.haxx.se/download/curl-7.61.1.tar.xz',
+    '3d5913d6a39bd22e68e34dff697fd6e4c3c81563f580c76fca2009315cd81891',
     'lib/libcurl.a',
     [
         '--disable-shared', '--enable-static',
diff --git a/src/command/DatabaseCommands.cxx b/src/command/DatabaseCommands.cxx
index 09c2612b4..f95c5eb3f 100644
--- a/src/command/DatabaseCommands.cxx
+++ b/src/command/DatabaseCommands.cxx
@@ -267,7 +267,7 @@ handle_list(Client &client, Request args, Response &r)
 	}
 
 	std::unique_ptr<SongFilter> filter;
-	TagMask group_mask = TagMask::None();
+	TagType group = TAG_NUM_OF_ITEM_TYPES;
 
 	if (args.size == 1) {
 		/* for compatibility with < 0.12.0 */
@@ -282,18 +282,16 @@ handle_list(Client &client, Request args, Response &r)
 					    args.shift()));
 	}
 
-	while (args.size >= 2 &&
-	       StringIsEqual(args[args.size - 2], "group")) {
+	if  (args.size >= 2 &&
+	     StringIsEqual(args[args.size - 2], "group")) {
 		const char *s = args[args.size - 1];
-		TagType gt = tag_name_parse_i(s);
-		if (gt == TAG_NUM_OF_ITEM_TYPES) {
+		group = tag_name_parse_i(s);
+		if (group == TAG_NUM_OF_ITEM_TYPES) {
 			r.FormatError(ACK_ERROR_ARG,
 				      "Unknown tag type: %s", s);
 			return CommandResult::ERROR;
 		}
 
-		group_mask |= gt;
-
 		args.pop_back();
 		args.pop_back();
 	}
@@ -310,13 +308,13 @@ handle_list(Client &client, Request args, Response &r)
 		filter->Optimize();
 	}
 
-	if (group_mask.Test(tagType)) {
+	if (tagType < TAG_NUM_OF_ITEM_TYPES && tagType == group) {
 		r.Error(ACK_ERROR_ARG, "Conflicting group");
 		return CommandResult::ERROR;
 	}
 
 	PrintUniqueTags(r, client.GetPartition(),
-			tagType, group_mask, filter.get());
+			tagType, group, filter.get());
 	return CommandResult::OK;
 }
 
diff --git a/src/db/Count.cxx b/src/db/Count.cxx
index 2e55821cb..a38f0934d 100644
--- a/src/db/Count.cxx
+++ b/src/db/Count.cxx
@@ -25,6 +25,7 @@
 #include "client/Response.hxx"
 #include "song/LightSong.hxx"
 #include "tag/Tag.hxx"
+#include "tag/VisitFallback.hxx"
 #include "TagPrint.hxx"
 
 #include <functional>
@@ -73,24 +74,15 @@ stats_visitor_song(SearchStats &stats, const LightSong &song) noexcept
 		stats.total_duration += duration;
 }
 
-static bool
-CollectGroupCounts(TagCountMap &map, TagType group, const Tag &tag) noexcept
+static void
+CollectGroupCounts(TagCountMap &map, const Tag &tag,
+		   const char *value) noexcept
 {
-	bool found = false;
-	for (const auto &item : tag) {
-		if (item.type == group) {
-			auto r = map.insert(std::make_pair(item.value,
-							   SearchStats()));
-			SearchStats &s = r.first->second;
-			++s.n_songs;
-			if (!tag.duration.IsNegative())
-				s.total_duration += tag.duration;
-
-			found = true;
-		}
-	}
-
-	return found;
+	auto r = map.insert(std::make_pair(value, SearchStats()));
+	SearchStats &s = r.first->second;
+	++s.n_songs;
+	if (!tag.duration.IsNegative())
+		s.total_duration += tag.duration;
 }
 
 static void
@@ -98,9 +90,10 @@ GroupCountVisitor(TagCountMap &map, TagType group,
 		  const LightSong &song) noexcept
 {
 	const Tag &tag = song.tag;
-	if (!CollectGroupCounts(map, group, tag) && group == TAG_ALBUM_ARTIST)
-		/* fall back to "Artist" if no "AlbumArtist" was found */
-		CollectGroupCounts(map, TAG_ARTIST, tag);
+	VisitTagWithFallbackOrEmpty(tag, group,
+				    std::bind(CollectGroupCounts, std::ref(map),
+					      std::cref(tag),
+					      std::placeholders::_1));
 }
 
 void
diff --git a/src/db/DatabasePrint.cxx b/src/db/DatabasePrint.cxx
index 9699dc442..78f32f940 100644
--- a/src/db/DatabasePrint.cxx
+++ b/src/db/DatabasePrint.cxx
@@ -187,22 +187,34 @@ PrintSongUris(Response &r, Partition &partition,
 }
 
 static void
-PrintUniqueTag(Response &r, TagType tag_type,
-	       const Tag &tag) noexcept
+PrintUniqueTags(Response &r, TagType tag_type,
+		const std::set<std::string> &values)
 {
-	const char *value = tag.GetValue(tag_type);
-	assert(value != nullptr);
-	tag_print(r, tag_type, value);
+	const char *const name = tag_item_names[tag_type];
+	for (const auto &i : values)
+		r.Format("%s: %s\n", name, i.c_str());
+}
 
-	const auto tag_mask = r.GetTagMask();
-	for (const auto &item : tag)
-		if (item.type != tag_type && tag_mask.Test(item.type))
-			tag_print(r, item.type, item.value);
+static void
+PrintGroupedUniqueTags(Response &r, TagType tag_type, TagType group,
+		       const std::map<std::string, std::set<std::string>> &groups)
+{
+	if (group == TAG_NUM_OF_ITEM_TYPES) {
+		for (const auto &i : groups)
+			PrintUniqueTags(r, tag_type, i.second);
+		return;
+	}
+
+	const char *const group_name = tag_item_names[group];
+	for (const auto &i : groups) {
+		r.Format("%s: %s\n", group_name, i.first.c_str());
+		PrintUniqueTags(r, tag_type, i.second);
+	}
 }
 
 void
 PrintUniqueTags(Response &r, Partition &partition,
-		TagType type, TagMask group_mask,
+		TagType type, TagType group,
 		const SongFilter *filter)
 {
 	assert(type < TAG_NUM_OF_ITEM_TYPES);
@@ -211,7 +223,6 @@ PrintUniqueTags(Response &r, Partition &partition,
 
 	const DatabaseSelection selection("", true, filter);
 
-	using namespace std::placeholders;
-	const auto f = std::bind(PrintUniqueTag, std::ref(r), type, _1);
-	db.VisitUniqueTags(selection, type, group_mask, f);
+	PrintGroupedUniqueTags(r, type, group,
+			       db.CollectUniqueTags(selection, type, group));
 }
diff --git a/src/db/DatabasePrint.hxx b/src/db/DatabasePrint.hxx
index 85670686c..b485ad787 100644
--- a/src/db/DatabasePrint.hxx
+++ b/src/db/DatabasePrint.hxx
@@ -45,7 +45,7 @@ PrintSongUris(Response &r, Partition &partition,
 
 void
 PrintUniqueTags(Response &r, Partition &partition,
-		TagType type, TagMask group_mask,
+		TagType type, TagType group,
 		const SongFilter *filter);
 
 #endif
diff --git a/src/db/Interface.hxx b/src/db/Interface.hxx
index 544162b9f..7b403d037 100644
--- a/src/db/Interface.hxx
+++ b/src/db/Interface.hxx
@@ -25,6 +25,9 @@
 #include "util/Compiler.h"
 
 #include <chrono>
+#include <map>
+#include <set>
+#include <string>
 
 struct DatabasePlugin;
 struct DatabaseStats;
@@ -105,14 +108,9 @@ public:
 		return Visit(selection, VisitDirectory(), visit_song);
 	}
 
-	/**
-	 * Visit all unique tag values.
-	 *
-	 * Throws on error.
-	 */
-	virtual void VisitUniqueTags(const DatabaseSelection &selection,
-				     TagType tag_type, TagMask group_mask,
-				     VisitTag visit_tag) const = 0;
+	virtual std::map<std::string, std::set<std::string>> CollectUniqueTags(const DatabaseSelection &selection,
+									       TagType tag_type,
+									       TagType group=TAG_NUM_OF_ITEM_TYPES) const = 0;
 
 	/**
 	 * Throws on error.
diff --git a/src/db/UniqueTags.cxx b/src/db/UniqueTags.cxx
index ad2ec2430..65b26e839 100644
--- a/src/db/UniqueTags.cxx
+++ b/src/db/UniqueTags.cxx
@@ -20,34 +20,42 @@
 #include "UniqueTags.hxx"
 #include "Interface.hxx"
 #include "song/LightSong.hxx"
-#include "tag/Set.hxx"
-#include "tag/Mask.hxx"
+#include "tag/VisitFallback.hxx"
 
 #include <functional>
 
 #include <assert.h>
 
 static void
-CollectTags(TagSet &set, TagType tag_type, TagMask group_mask,
-	    const LightSong &song)
+CollectTags(std::set<std::string> &result,
+	    const Tag &tag,
+	    TagType tag_type) noexcept
 {
-	const Tag &tag = song.tag;
-
-	set.InsertUnique(tag, tag_type, group_mask);
+	VisitTagWithFallbackOrEmpty(tag, tag_type, [&result](const char *value){
+			result.emplace(value);
+		});
 }
 
-void
-VisitUniqueTags(const Database &db, const DatabaseSelection &selection,
-		TagType tag_type, TagMask group_mask,
-		VisitTag visit_tag)
+static void
+CollectGroupTags(std::map<std::string, std::set<std::string>> &result,
+		 const Tag &tag,
+		 TagType tag_type,
+		 TagType group) noexcept
 {
-	TagSet set;
-
-	using namespace std::placeholders;
-	const auto f = std::bind(CollectTags, std::ref(set),
-				 tag_type, group_mask, _1);
-	db.Visit(selection, f);
-
-	for (const auto &value : set)
-		visit_tag(value);
+	VisitTagWithFallbackOrEmpty(tag, group, [&](const char *group_name){
+			CollectTags(result[group_name], tag, tag_type);
+		});
+}
+
+std::map<std::string, std::set<std::string>>
+CollectUniqueTags(const Database &db, const DatabaseSelection &selection,
+		  TagType tag_type, TagType group)
+{
+	std::map<std::string, std::set<std::string>> result;
+
+	db.Visit(selection, [&result, tag_type, group](const LightSong &song){
+			CollectGroupTags(result, song.tag, tag_type, group);
+		});
+
+	return result;
 }
diff --git a/src/db/UniqueTags.hxx b/src/db/UniqueTags.hxx
index 0790acb3d..d646ab3c2 100644
--- a/src/db/UniqueTags.hxx
+++ b/src/db/UniqueTags.hxx
@@ -20,16 +20,20 @@
 #ifndef MPD_DB_UNIQUE_TAGS_HXX
 #define MPD_DB_UNIQUE_TAGS_HXX
 
-#include "Visitor.hxx"
 #include "tag/Type.h"
+#include "util/Compiler.h"
+
+#include <map>
+#include <set>
+#include <string>
 
 class TagMask;
 class Database;
 struct DatabaseSelection;
 
-void
-VisitUniqueTags(const Database &db, const DatabaseSelection &selection,
-		TagType tag_type, TagMask group_mask,
-		VisitTag visit_tag);
+gcc_pure
+std::map<std::string, std::set<std::string>>
+CollectUniqueTags(const Database &db, const DatabaseSelection &selection,
+		  TagType tag_type, TagType group);
 
 #endif
diff --git a/src/db/plugins/ProxyDatabasePlugin.cxx b/src/db/plugins/ProxyDatabasePlugin.cxx
index b1a63a6a6..ca249147b 100644
--- a/src/db/plugins/ProxyDatabasePlugin.cxx
+++ b/src/db/plugins/ProxyDatabasePlugin.cxx
@@ -128,9 +128,9 @@ public:
 		   VisitSong visit_song,
 		   VisitPlaylist visit_playlist) const override;
 
-	void VisitUniqueTags(const DatabaseSelection &selection,
-			     TagType tag_type, TagMask group_mask,
-			     VisitTag visit_tag) const override;
+	std::map<std::string, std::set<std::string>> CollectUniqueTags(const DatabaseSelection &selection,
+								       TagType tag_type,
+								       TagType group) const override;
 
 	DatabaseStats GetStats(const DatabaseSelection &selection) const override;
 
@@ -411,31 +411,21 @@ SendConstraints(mpd_connection *connection, const DatabaseSelection &selection)
 }
 
 static bool
-SendGroupMask(mpd_connection *connection, TagMask mask)
+SendGroup(mpd_connection *connection, TagType group)
 {
+	if (group == TAG_NUM_OF_ITEM_TYPES)
+		return true;
+
 #if LIBMPDCLIENT_CHECK_VERSION(2,12,0)
-	for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i) {
-		const auto tag_type = TagType(i);
-		if (!mask.Test(tag_type))
-			continue;
+	const auto tag = Convert(group);
+	if (tag == MPD_TAG_COUNT)
+		throw std::runtime_error("Unsupported tag");
 
-		const auto tag = Convert(tag_type);
-		if (tag == MPD_TAG_COUNT)
-			throw std::runtime_error("Unsupported tag");
-
-		if (!mpd_search_add_group_tag(connection, tag))
-			return false;
-	}
-
-	return true;
+	return mpd_search_add_group_tag(connection, tag);
 #else
 	(void)connection;
-	(void)mask;
 
-	if (mask.TestAny())
-		throw std::runtime_error("Grouping requires libmpdclient 2.12");
-
-	return true;
+	throw std::runtime_error("Grouping requires libmpdclient 2.12");
 #endif
 }
 
@@ -992,11 +982,9 @@ ProxyDatabase::Visit(const DatabaseSelection &selection,
 	helper.Commit();
 }
 
-void
-ProxyDatabase::VisitUniqueTags(const DatabaseSelection &selection,
-			       TagType tag_type,
-			       TagMask group_mask,
-			       VisitTag visit_tag) const
+std::map<std::string, std::set<std::string>>
+ProxyDatabase::CollectUniqueTags(const DatabaseSelection &selection,
+				 TagType tag_type, TagType group) const
 try {
 	// TODO: eliminate the const_cast
 	const_cast<ProxyDatabase *>(this)->EnsureConnected();
@@ -1007,54 +995,56 @@ try {
 
 	if (!mpd_search_db_tags(connection, tag_type2) ||
 	    !SendConstraints(connection, selection) ||
-	    !SendGroupMask(connection, group_mask))
+	    !SendGroup(connection, group))
 		ThrowError(connection);
 
 	if (!mpd_search_commit(connection))
 		ThrowError(connection);
 
-	TagBuilder builder;
+	std::map<std::string, std::set<std::string>> result;
 
-	while (auto *pair = mpd_recv_pair(connection)) {
-		AtScopeExit(this, pair) {
-			mpd_return_pair(connection, pair);
-		};
+	if (group == TAG_NUM_OF_ITEM_TYPES) {
+		auto &values = result[std::string()];
 
-		const auto current_type = tag_name_parse_i(pair->name);
-		if (current_type == TAG_NUM_OF_ITEM_TYPES)
-			continue;
+		while (auto *pair = mpd_recv_pair(connection)) {
+			AtScopeExit(this, pair) {
+				mpd_return_pair(connection, pair);
+			};
 
-		if (current_type == tag_type && !builder.empty()) {
-			try {
-				visit_tag(builder.Commit());
-			} catch (...) {
-				mpd_response_finish(connection);
-				throw;
-			}
+			const auto current_type = tag_name_parse_i(pair->name);
+			if (current_type == TAG_NUM_OF_ITEM_TYPES)
+				continue;
+
+			if (current_type == tag_type)
+				values.emplace(pair->value);
 		}
+	} else {
+		std::set<std::string> *current_group = nullptr;
 
-		builder.AddItem(current_type, pair->value);
+		while (auto *pair = mpd_recv_pair(connection)) {
+			AtScopeExit(this, pair) {
+				mpd_return_pair(connection, pair);
+			};
 
-		if (!builder.HasType(current_type))
-			/* if no tag item has been added, then the
-			   given value was not acceptable
-			   (e.g. empty); forcefully insert an empty
-			   tag in this case, as the caller expects the
-			   given tag type to be present */
-			builder.AddEmptyItem(current_type);
-	}
+			const auto current_type = tag_name_parse_i(pair->name);
+			if (current_type == TAG_NUM_OF_ITEM_TYPES)
+				continue;
 
-	if (!builder.empty()) {
-		try {
-			visit_tag(builder.Commit());
-		} catch (...) {
-			mpd_response_finish(connection);
-			throw;
+			if (current_type == tag_type) {
+				if (current_group == nullptr)
+					current_group = &result[std::string()];
+
+				current_group->emplace(pair->value);
+			} else if (current_type == group) {
+				current_group = &result[pair->value];
+			}
 		}
 	}
 
 	if (!mpd_response_finish(connection))
 		ThrowError(connection);
+
+	return result;
 } catch (...) {
 	if (connection != nullptr)
 		mpd_search_cancel(connection);
diff --git a/src/db/plugins/simple/SimpleDatabasePlugin.cxx b/src/db/plugins/simple/SimpleDatabasePlugin.cxx
index 5fdc4ec80..daf75654c 100644
--- a/src/db/plugins/simple/SimpleDatabasePlugin.cxx
+++ b/src/db/plugins/simple/SimpleDatabasePlugin.cxx
@@ -329,12 +329,11 @@ SimpleDatabase::Visit(const DatabaseSelection &selection,
 			    "No such directory");
 }
 
-void
-SimpleDatabase::VisitUniqueTags(const DatabaseSelection &selection,
-				TagType tag_type, TagMask group_mask,
-				VisitTag visit_tag) const
+std::map<std::string, std::set<std::string>>
+SimpleDatabase::CollectUniqueTags(const DatabaseSelection &selection,
+				  TagType tag_type, TagType group) const
 {
-	::VisitUniqueTags(*this, selection, tag_type, group_mask, visit_tag);
+	return ::CollectUniqueTags(*this, selection, tag_type, group);
 }
 
 DatabaseStats
diff --git a/src/db/plugins/simple/SimpleDatabasePlugin.hxx b/src/db/plugins/simple/SimpleDatabasePlugin.hxx
index b95b7f5f6..be3055ed7 100644
--- a/src/db/plugins/simple/SimpleDatabasePlugin.hxx
+++ b/src/db/plugins/simple/SimpleDatabasePlugin.hxx
@@ -122,9 +122,9 @@ public:
 		   VisitSong visit_song,
 		   VisitPlaylist visit_playlist) const override;
 
-	void VisitUniqueTags(const DatabaseSelection &selection,
-			     TagType tag_type, TagMask group_mask,
-			     VisitTag visit_tag) const override;
+	std::map<std::string, std::set<std::string>> CollectUniqueTags(const DatabaseSelection &selection,
+								       TagType tag_type,
+								       TagType group) const override;
 
 	DatabaseStats GetStats(const DatabaseSelection &selection) const override;
 
diff --git a/src/db/plugins/upnp/UpnpDatabasePlugin.cxx b/src/db/plugins/upnp/UpnpDatabasePlugin.cxx
index 9d6a64f04..01ae971aa 100644
--- a/src/db/plugins/upnp/UpnpDatabasePlugin.cxx
+++ b/src/db/plugins/upnp/UpnpDatabasePlugin.cxx
@@ -97,9 +97,9 @@ public:
 		   VisitSong visit_song,
 		   VisitPlaylist visit_playlist) const override;
 
-	void VisitUniqueTags(const DatabaseSelection &selection,
-			     TagType tag_type, TagMask group_mask,
-			     VisitTag visit_tag) const override;
+	std::map<std::string, std::set<std::string>> CollectUniqueTags(const DatabaseSelection &selection,
+								       TagType tag_type,
+								       TagType group) const override;
 
 	DatabaseStats GetStats(const DatabaseSelection &selection) const override;
 
@@ -624,17 +624,15 @@ UpnpDatabase::Visit(const DatabaseSelection &selection,
 	helper.Commit();
 }
 
-void
-UpnpDatabase::VisitUniqueTags(const DatabaseSelection &selection,
-			      TagType tag, gcc_unused TagMask group_mask,
-			      VisitTag visit_tag) const
+std::map<std::string, std::set<std::string>>
+UpnpDatabase::CollectUniqueTags(const DatabaseSelection &selection,
+				TagType tag, TagType group) const
 {
-	// TODO: use group_mask
+	(void)group; // TODO: use group
 
-	if (!visit_tag)
-		return;
+	std::map<std::string, std::set<std::string>> result;
+	auto &values = result[std::string()];
 
-	std::set<std::string> values;
 	for (auto& server : discovery->GetDirectories()) {
 		const auto dirbuf = SearchSongs(server, rootid, selection);
 
@@ -650,11 +648,7 @@ UpnpDatabase::VisitUniqueTags(const DatabaseSelection &selection,
 		}
 	}
 
-	for (const auto& value : values) {
-		TagBuilder builder;
-		builder.AddItem(tag, value.c_str());
-		visit_tag(builder.Commit());
-	}
+	return result;
 }
 
 DatabaseStats
diff --git a/src/song/TagSongFilter.cxx b/src/song/TagSongFilter.cxx
index 35939ee8a..e77169ddc 100644
--- a/src/song/TagSongFilter.cxx
+++ b/src/song/TagSongFilter.cxx
@@ -21,6 +21,7 @@
 #include "TagSongFilter.hxx"
 #include "LightSong.hxx"
 #include "tag/Tag.hxx"
+#include "tag/Fallback.hxx"
 
 std::string
 TagSongFilter::ToExpression() const noexcept
@@ -62,14 +63,23 @@ TagSongFilter::MatchNN(const Tag &tag) const noexcept
 		if (filter.empty())
 			return true;
 
-		if (type == TAG_ALBUM_ARTIST && visited_types[TAG_ARTIST]) {
-			/* if we're looking for "album artist", but
-			   only "artist" exists, use that */
-			for (const auto &item : tag)
-				if (item.type == TAG_ARTIST &&
-				    filter.Match(item.value))
-					return true;
-		}
+		bool result = false;
+		if (ApplyTagFallback(type,
+				     [&](TagType tag2) {
+			     if (!visited_types[tag2])
+				     return false;
+
+			     for (const auto &item : tag) {
+				     if (item.type == tag2 &&
+					 filter.Match(item.value)) {
+					     result = true;
+					     break;
+				     }
+			     }
+
+			     return true;
+		     }))
+			return result;
 	}
 
 	return false;
diff --git a/src/tag/Builder.cxx b/src/tag/Builder.cxx
index 1bcdb5bda..85a77131b 100644
--- a/src/tag/Builder.cxx
+++ b/src/tag/Builder.cxx
@@ -186,6 +186,19 @@ TagBuilder::Complement(const Tag &other) noexcept
 	}
 }
 
+void
+TagBuilder::AddItemUnchecked(TagType type, StringView value) noexcept
+{
+	TagItem *i;
+
+	{
+		const std::lock_guard<Mutex> protect(tag_pool_lock);
+		i = tag_pool_get_item(type, value);
+	}
+
+	items.push_back(i);
+}
+
 inline void
 TagBuilder::AddItemInternal(TagType type, StringView value) noexcept
 {
@@ -195,15 +208,9 @@ TagBuilder::AddItemInternal(TagType type, StringView value) noexcept
 	if (!f.IsNull())
 		value = { f.data, f.size };
 
-	TagItem *i;
-	{
-		const std::lock_guard<Mutex> protect(tag_pool_lock);
-		i = tag_pool_get_item(type, value);
-	}
+	AddItemUnchecked(type, value);
 
 	free(f.data);
-
-	items.push_back(i);
 }
 
 void
@@ -229,13 +236,7 @@ TagBuilder::AddItem(TagType type, const char *value) noexcept
 void
 TagBuilder::AddEmptyItem(TagType type) noexcept
 {
-	TagItem *i;
-	{
-		const std::lock_guard<Mutex> protect(tag_pool_lock);
-		i = tag_pool_get_item(type, "");
-	}
-
-	items.push_back(i);
+	AddItemUnchecked(type, "");
 }
 
 void
diff --git a/src/tag/Builder.hxx b/src/tag/Builder.hxx
index 136e0e745..dbf4cddb8 100644
--- a/src/tag/Builder.hxx
+++ b/src/tag/Builder.hxx
@@ -132,6 +132,12 @@ public:
 	 */
 	void Complement(const Tag &other) noexcept;
 
+	/**
+	 * A variant of AddItem() which does not attempt to fix up the
+	 * value and does not check whether the tag type is disabled.
+	 */
+	void AddItemUnchecked(TagType type, StringView value) noexcept;
+
 	/**
 	 * Appends a new tag item.
 	 *
diff --git a/src/tag/Fallback.hxx b/src/tag/Fallback.hxx
new file mode 100644
index 000000000..08e4dc80a
--- /dev/null
+++ b/src/tag/Fallback.hxx
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2003-2018 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_TAG_FALLBACK_HXX
+#define MPD_TAG_FALLBACK_HXX
+
+#include <utility>
+
+template<typename F>
+bool
+ApplyTagFallback(TagType type, F &&f) noexcept
+{
+	if (type == TAG_ALBUM_ARTIST_SORT) {
+		/* fall back to "AlbumArtist", "ArtistSort" and
+		   "Artist" if no "AlbumArtistSort" was found */
+		if (f(TAG_ALBUM_ARTIST))
+			return true;
+
+		return ApplyTagFallback(TAG_ARTIST_SORT, std::forward<F>(f));
+	}
+
+	if (type == TAG_ALBUM_ARTIST || type == TAG_ARTIST_SORT)
+		/* fall back to "Artist" if no
+		   "AlbumArtist"/"ArtistSort" was found */
+		return f(TAG_ARTIST);
+
+	return false;
+}
+
+template<typename F>
+bool
+ApplyTagWithFallback(TagType type, F &&f) noexcept
+{
+	return f(type) || ApplyTagFallback(type, std::forward<F>(f));
+}
+
+#endif
diff --git a/src/tag/Set.cxx b/src/tag/Set.cxx
deleted file mode 100644
index 0009173bb..000000000
--- a/src/tag/Set.cxx
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright 2003-2017 The Music Player Daemon Project
- * http://www.musicpd.org
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#include "Set.hxx"
-#include "Builder.hxx"
-#include "Mask.hxx"
-#include "Settings.hxx"
-
-#include <assert.h>
-
-/**
- * Copy all tag items of the specified type.
- */
-static bool
-CopyTagItem(TagBuilder &dest, TagType dest_type,
-	    const Tag &src, TagType src_type)
-{
-	bool found = false;
-
-	for (const auto &item : src) {
-		if (item.type == src_type) {
-			dest.AddItem(dest_type, item.value);
-			found = true;
-		}
-	}
-
-	return found;
-}
-
-/**
- * Copy all tag items of the specified type.  Fall back to "Artist" if
- * there is no "AlbumArtist".
- */
-static void
-CopyTagItem(TagBuilder &dest, const Tag &src, TagType type)
-{
-	if (!CopyTagItem(dest, type, src, type) &&
-	    type == TAG_ALBUM_ARTIST)
-		CopyTagItem(dest, type, src, TAG_ARTIST);
-}
-
-/**
- * Copy all tag items of the types in the mask.
- */
-static void
-CopyTagMask(TagBuilder &dest, const Tag &src, TagMask mask)
-{
-	for (unsigned i = 0; i < TAG_NUM_OF_ITEM_TYPES; ++i)
-		if (mask.Test(TagType(i)))
-			CopyTagItem(dest, src, TagType(i));
-}
-
-void
-TagSet::InsertUnique(const Tag &src, TagType type, const char *value,
-		     TagMask group_mask) noexcept
-{
-	TagBuilder builder;
-	if (value == nullptr)
-		builder.AddEmptyItem(type);
-	else
-		builder.AddItem(type, value);
-	CopyTagMask(builder, src, group_mask);
-	emplace(builder.Commit());
-}
-
-bool
-TagSet::CheckUnique(TagType dest_type,
-		    const Tag &tag, TagType src_type,
-		    TagMask group_mask) noexcept
-{
-	bool found = false;
-
-	for (const auto &item : tag) {
-		if (item.type == src_type) {
-			InsertUnique(tag, dest_type, item.value, group_mask);
-			found = true;
-		}
-	}
-
-	return found;
-}
-
-void
-TagSet::InsertUnique(const Tag &tag,
-		     TagType type, TagMask group_mask) noexcept
-{
-	static_assert(sizeof(group_mask) * 8 >= TAG_NUM_OF_ITEM_TYPES,
-		      "Mask is too small");
-
-	assert(!group_mask.Test(type));
-
-	if (!CheckUnique(type, tag, type, group_mask) &&
-	    (type != TAG_ALBUM_ARTIST ||
-	     !IsTagEnabled(TAG_ALBUM_ARTIST) ||
-	     /* fall back to "Artist" if no "AlbumArtist" was found */
-	     !CheckUnique(type, tag, TAG_ARTIST, group_mask)))
-		InsertUnique(tag, type, nullptr, group_mask);
-}
diff --git a/src/tag/Set.hxx b/src/tag/Set.hxx
deleted file mode 100644
index e74d145f4..000000000
--- a/src/tag/Set.hxx
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2003-2017 The Music Player Daemon Project
- * http://www.musicpd.org
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
- */
-
-#ifndef MPD_TAG_SET_HXX
-#define MPD_TAG_SET_HXX
-
-#include "util/Compiler.h"
-#include "Tag.hxx"
-
-#include <set>
-
-#include <string.h>
-
-class TagMask;
-
-/**
- * Helper class for #TagSet which compares two #Tag objects.
- */
-struct TagLess {
-	gcc_pure
-	bool operator()(const Tag &a, const Tag &b) const noexcept {
-		if (a.num_items != b.num_items)
-			return a.num_items < b.num_items;
-
-		const unsigned n = a.num_items;
-		for (unsigned i = 0; i < n; ++i) {
-			const TagItem &ai = *a.items[i];
-			const TagItem &bi = *b.items[i];
-			if (ai.type != bi.type)
-				return unsigned(ai.type) < unsigned(bi.type);
-
-			const int cmp = strcmp(ai.value, bi.value);
-			if (cmp != 0)
-				return cmp < 0;
-		}
-
-		return false;
-	}
-};
-
-/**
- * A set of #Tag objects.
- */
-class TagSet : public std::set<Tag, TagLess> {
-public:
-	void InsertUnique(const Tag &tag,
-			  TagType type, TagMask group_mask) noexcept;
-
-private:
-	void InsertUnique(const Tag &src, TagType type, const char *value,
-			  TagMask group_mask) noexcept;
-
-	bool CheckUnique(TagType dest_type,
-			 const Tag &tag, TagType src_type,
-			 TagMask group_mask) noexcept;
-};
-
-#endif
diff --git a/src/tag/VisitFallback.hxx b/src/tag/VisitFallback.hxx
new file mode 100644
index 000000000..23992d44b
--- /dev/null
+++ b/src/tag/VisitFallback.hxx
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2003-2018 The Music Player Daemon Project
+ * http://www.musicpd.org
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_TAG_VISIT_FALLBACK_HXX
+#define MPD_TAG_VISIT_FALLBACK_HXX
+
+#include "Fallback.hxx"
+#include "Tag.hxx"
+
+template<typename F>
+bool
+VisitTagType(const Tag &tag, TagType type, F &&f) noexcept
+{
+	bool found = false;
+
+	for (const auto &item : tag) {
+		if (item.type == type) {
+			found = true;
+			f(item.value);
+		}
+	}
+
+	return found;
+}
+
+template<typename F>
+bool
+VisitTagWithFallback(const Tag &tag, TagType type, F &&f) noexcept
+{
+	return ApplyTagWithFallback(type,
+				    [&](TagType type2) {
+					    return VisitTagType(tag, type2, f);
+				    });
+}
+
+template<typename F>
+void
+VisitTagWithFallbackOrEmpty(const Tag &tag, TagType type, F &&f) noexcept
+{
+	if (!VisitTagWithFallback(tag, type, f))
+		f("");
+}
+
+#endif
diff --git a/src/tag/meson.build b/src/tag/meson.build
index b0121a71d..c423417d1 100644
--- a/src/tag/meson.build
+++ b/src/tag/meson.build
@@ -9,7 +9,6 @@ tag_sources = [
   'FixString.cxx',
   'Pool.cxx',
   'Table.cxx',
-  'Set.cxx',
   'Format.cxx',
   'VorbisComment.cxx',
   'ReplayGain.cxx',