I emailed Michael and Kato off-list to clarify some additional questions I had (thanks for the help!), and below is a summary of my current understanding.
Assume that postsRef is some path in your database ("/posts"), and we have the following two queries:
[a] postsRef.orderByChild("slug").equalTo("my-first-post")
[b] postsRef.orderBy("publishDate").limit(10)
We can say that [a] and [b] are overlapping, because they are on the same branch, but independent and would not share internal cache, because we have no way of knowing in advance whether [b] would contain [a]. So even though query [b] may have already downloaded the post we're looking for in [a], [a] will hit the server again.
However, we can create the following listener, with an empty callback, to ensure that we've always got a local cache of the entire branch:
[c] postsRef.on("value", function(){})
With this listener in place, any query we run which merely filters or traverses it would use the existing cached data. So the question internally is, "do we already have this entire tree? If yes, query the local cache. If not, hit the server." Given an existing listener [c], queries [a] and [b] will be local, cached queries.
Michael also had this to add about indexes on the client:
As for indexes, we'll basically create them as-needed in the client. As soon as you do .orderByChild("slug"), we'll start maintaining an in-memory index ordered by "slug." This may cause a (very slight) hiccup when you start your query, but subsequent updates (as data changes) will just be incremental index updates and be just as fast as handling any other data change. In practice, the amount of data you're dealing with on the client is small enough that doing some in-memory sorting is no big deal.
My other question, about accessing the data returned, was a bit muddled because my data did, in fact, look like an array - I didn't realize it was significant at the time, but in Forge I had typed in 1, 2, 3, 4, 5... as keys, different from the sample data I typed into my question, which meant that Firebase interpreted it as a list. So that was part of my problem - not being clear about whether I was expecting an array or an object as the result.
Still, there is something to be learned: with a query like q = ref.orderByChild("slug").limitToFirst(1) you are clearly expecting a single result, but if you run q.on("value", function(snap){...}), the snapshot is still one level up from the result you want. It might look like { '-JiHrDOr6HoEzOVibPoN': { host: 'www.apple.com' } } where in fact you want just { host: 'www.apple.com' }. One way to get around this is to use "child_added" instead, which will be called with a snapshot just of the document you want. However, as Michael warns: "The one potential caveat is if you do ref.orderBy('host').equalTo('foo').once('child_added', ...) but there is no item with host 'foo', then you won't get any callback (unless an item with host 'foo' is created later). So it's not possible to detect the case where there are no matches for your query."