What's happening under the hood will be similar, but Cypher is declarative, not imperative. It's important to understand that most Cypher operations (such as matches and such) execute per row, and yield rows. So your initial MATCH to a
p:Person node will generate a row per path, extracting the
p variable, so subsequent operations, since they execute per row, would effectively execute per person.
We could improve the query a bit with subqueries such that further work happens scoped per person, that works more like your looping approach, and can also keep the aggregation cheap, so instead of doing a large aggregation across the full result set from your initial MATCH, it would be doing many small aggregations, one per person. We can look at that a bit later, that's an optimization, but we need to address the issue of bad results.
The problem is with your grouping key. The grouping key is the context for the aggregation, and when we perform an aggregation (such as max(), collect(), count(), and any of the APOC agg functions, such as
apoc.agg.minItems()), the non-aggregation terms form the grouping key. Your issue is that
Laws, there's some inconsistency so the variables you're using) is present when you aggregate, so it's part of the grouping key. It's calculating the minItems() per combo of person / law, which is not what you want. You need to remove Law from your WITH clause when you aggregate, then the minItems() aggregation will be grouped per person.